From 7e7add13430ae5b7e9609ae1443c7cdbb4b0c522 Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Mon, 16 Mar 2026 13:32:56 +0800 Subject: [PATCH 1/3] docs: add downstream key ui and site link plan --- ...downstream-key-ui-and-site-links-design.md | 67 ++++++++ ...-03-16-downstream-key-ui-and-site-links.md | 160 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md create mode 100644 docs/plans/2026-03-16-downstream-key-ui-and-site-links.md diff --git a/docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md b/docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md new file mode 100644 index 0000000..5d1c2a4 --- /dev/null +++ b/docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md @@ -0,0 +1,67 @@ +# Downstream Key UI And Site Links Design + +**Goal:** Unify the downstream key page with the restrained management-console style already used by the sites and accounts pages, and make site entry points behave consistently across pages. + +**Context** + +The current downstream key page is functionally rich but visually heavier than the rest of the console. It relies on dense inline styling and presents summary, filters, and row content with a stronger dashboard tone than the nearby management pages. + +At the same time, site references are inconsistent. The accounts page already exposes a hoverable, clickable site badge that navigates back to site management with focus context. Other pages such as models, proxy logs, token routes, and downstream keys do not consistently reuse that interaction. + +**User-Approved Direction** + +- Align downstream key visuals with the existing `站点管理 / 连接管理` style. +- Reuse the same site button interaction already present on the accounts page. +- Prefer direct reuse over inventing a new visual pattern. + +## Design + +### 1. Downstream key page visual alignment + +Keep the data model and operations unchanged, but reduce the visual mismatch: + +- Keep the existing `page-header`, `card`, and `data-table` structure. +- Restyle summary/filter regions to read like management controls rather than dashboard hero sections. +- Preserve the drawer/details flow, but make the main list view feel closer to `Sites.tsx` and `Accounts.tsx`. +- Replace repeated ad-hoc chip/button styling where possible with the same badge/link language already used elsewhere. + +This is intentionally a presentation cleanup, not a workflow redesign. + +### 2. Shared site button behavior + +Extract the accounts page site badge behavior into a reusable component: + +- Same hover affordance. +- Same visual style. +- Same navigation target: `/sites?focusSiteId=`. +- Graceful fallback when `siteId` is missing or invalid. + +Pages to update: + +- `src/web/pages/Models.tsx` +- `src/web/pages/ProxyLogs.tsx` +- `src/web/pages/TokenRoutes.tsx` +- `src/web/pages/DownstreamKeys.tsx` + +The `Sites.tsx` page itself continues to use direct external URL links, because those links serve a different purpose. + +### 3. Testing approach + +Cover the change with focused UI tests: + +- One shared component test for hover/click navigation behavior. +- Page-level regression tests where existing snapshots/assertions are already present. +- Keep tests narrow: validate that site labels render with the shared class/behavior and navigate to the focused site management route. + +## Risks + +- `TokenRoutes.tsx` contains multiple kinds of site labels; only labels intended to behave like the accounts page should be converted. +- `DownstreamKeys.tsx` may not currently expose enough site metadata in every row; if site links are only meaningful in detail regions, apply the shared component there instead of forcing low-value links into the table. +- Visual cleanup should not destabilize mobile layout or row actions. + +## Acceptance Criteria + +- The downstream key page looks consistent with the restrained management-console pages. +- Site entry points in models, proxy logs, token routes, and downstream keys match the accounts page interaction. +- Hover and navigation behavior are consistent. +- Existing relevant tests pass, and new tests cover the shared site link behavior. diff --git a/docs/plans/2026-03-16-downstream-key-ui-and-site-links.md b/docs/plans/2026-03-16-downstream-key-ui-and-site-links.md new file mode 100644 index 0000000..902228e --- /dev/null +++ b/docs/plans/2026-03-16-downstream-key-ui-and-site-links.md @@ -0,0 +1,160 @@ +# Downstream Key UI And Site Links Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Restyle the downstream key page to match the restrained management-console pages and standardize site buttons across pages using the accounts page interaction. + +**Architecture:** Introduce one shared site link component based on the accounts page behavior, then update each affected page to consume it. Refactor downstream key layout styling toward existing `card`, `badge`, and `data-table` patterns without changing backend behavior. + +**Tech Stack:** React, TypeScript, React Router, Vitest, React Test Renderer + +--- + +### Task 1: Extract the shared site button + +**Files:** +- Create: `src/web/components/SiteBadgeLink.tsx` +- Test: `src/web/components/site-badge-link.test.tsx` +- Modify: `src/web/pages/Accounts.tsx` +- Modify: `src/web/index.css` + +**Step 1: Write the failing test** + +Add a component test that renders a site badge link and asserts: +- it renders the site name, +- it has the shared interactive class, +- clicking navigates to `/sites?focusSiteId=`. + +**Step 2: Run test to verify it fails** + +Run: `npm test -- src/web/components/site-badge-link.test.tsx` + +Expected: FAIL because the component does not exist yet. + +**Step 3: Write minimal implementation** + +- Create a small component that wraps `Link` from `react-router-dom`. +- Accept `siteId`, `siteName`, and optional badge class props. +- Reproduce the accounts page badge-link styling and fallback behavior. +- Move the accounts page site badge rendering to this component. +- Add any missing shared CSS class if accounts currently relies on page-local styling. + +**Step 4: Run test to verify it passes** + +Run: `npm test -- src/web/components/site-badge-link.test.tsx src/web/pages/accounts.segmented-connections.test.tsx` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/web/components/SiteBadgeLink.tsx src/web/components/site-badge-link.test.tsx src/web/pages/Accounts.tsx src/web/index.css +git commit -m "refactor: extract shared site badge link" +``` + +### Task 2: Apply the shared site button across affected pages + +**Files:** +- Modify: `src/web/pages/Models.tsx` +- Modify: `src/web/pages/ProxyLogs.tsx` +- Modify: `src/web/pages/TokenRoutes.tsx` +- Modify: `src/web/pages/DownstreamKeys.tsx` +- Test: `src/web/pages/Models.marketplace-text.test.tsx` +- Test: `src/web/pages/tokenRoutes.group-collapse.test.tsx` +- Test: `src/web/pages/DownstreamKeys.test.tsx` + +**Step 1: Write failing assertions** + +Add or update tests to assert the affected pages render the shared site button class and, where practical, point to the focused site management route. + +**Step 2: Run test to verify it fails** + +Run: `npm test -- src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx` + +Expected: FAIL because the pages still use inconsistent site rendering. + +**Step 3: Write minimal implementation** + +- Replace page-local site label rendering with `SiteBadgeLink`. +- Only convert labels that represent navigable management-site references. +- Preserve non-site filters, external URLs, and unrelated badges. + +**Step 4: Run test to verify it passes** + +Run: `npm test -- src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx src/web/pages/ProxyLogs.server-driven.test.tsx` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/web/pages/Models.tsx src/web/pages/ProxyLogs.tsx src/web/pages/TokenRoutes.tsx src/web/pages/DownstreamKeys.tsx src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx +git commit -m "feat: unify site badge navigation across pages" +``` + +### Task 3: Restyle downstream keys page + +**Files:** +- Modify: `src/web/pages/DownstreamKeys.tsx` +- Test: `src/web/pages/DownstreamKeys.test.tsx` +- Modify: `src/web/index.css` + +**Step 1: Write failing assertions** + +Add focused assertions for the downstream keys page structure where needed, such as shared management-page card classes, reduced bespoke styling hooks, or the new site badge placement. + +**Step 2: Run test to verify it fails** + +Run: `npm test -- src/web/pages/DownstreamKeys.test.tsx` + +Expected: FAIL after adding assertions that reflect the updated restrained layout. + +**Step 3: Write minimal implementation** + +- Reduce the page’s dashboard-like presentation. +- Align summary, filters, and batch action sections with the same management-console card language used in sites/accounts. +- Reuse shared badge/link patterns instead of one-off visual treatments where practical. +- Keep operations, data fetching, and drawer behavior unchanged. + +**Step 4: Run test to verify it passes** + +Run: `npm test -- src/web/pages/DownstreamKeys.test.tsx` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/web/pages/DownstreamKeys.tsx src/web/pages/DownstreamKeys.test.tsx src/web/index.css +git commit -m "style: align downstream key page with management UI" +``` + +### Task 4: Final verification + +**Files:** +- Modify: none + +**Step 1: Run targeted verification** + +Run: +- `npm test -- src/web/components/site-badge-link.test.tsx` +- `npm test -- src/web/pages/DownstreamKeys.test.tsx` +- `npm test -- src/web/pages/Models.marketplace-text.test.tsx` +- `npm test -- src/web/pages/tokenRoutes.group-collapse.test.tsx` +- `npm test -- src/web/pages/ProxyLogs.server-driven.test.tsx` +- `npm test -- src/web/pages/accounts.segmented-connections.test.tsx` + +Expected: PASS + +**Step 2: Run broader sanity verification if time permits** + +Run: `npm test -- src/web/App.sidebar.test.ts src/web/App.sidebar-mobile.test.tsx` + +Expected: PASS + +**Step 3: Commit final polish** + +```bash +git add -A +git commit -m "test: verify downstream key UI and site link consistency" +``` From bfb7ea180f8c1f0b8e36d9fa1ed04b432bb77c69 Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Mon, 16 Mar 2026 14:41:29 +0800 Subject: [PATCH 2/3] Refine DownstreamKeys management page --- ...downstream-key-ui-and-site-links-design.md | 67 --- src/web/index.css | 145 +++++- src/web/pages/DownstreamKeys.test.tsx | 60 ++- src/web/pages/DownstreamKeys.tsx | 439 +++++++++--------- 4 files changed, 425 insertions(+), 286 deletions(-) delete mode 100644 docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md diff --git a/docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md b/docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md deleted file mode 100644 index 5d1c2a4..0000000 --- a/docs/plans/2026-03-16-downstream-key-ui-and-site-links-design.md +++ /dev/null @@ -1,67 +0,0 @@ -# Downstream Key UI And Site Links Design - -**Goal:** Unify the downstream key page with the restrained management-console style already used by the sites and accounts pages, and make site entry points behave consistently across pages. - -**Context** - -The current downstream key page is functionally rich but visually heavier than the rest of the console. It relies on dense inline styling and presents summary, filters, and row content with a stronger dashboard tone than the nearby management pages. - -At the same time, site references are inconsistent. The accounts page already exposes a hoverable, clickable site badge that navigates back to site management with focus context. Other pages such as models, proxy logs, token routes, and downstream keys do not consistently reuse that interaction. - -**User-Approved Direction** - -- Align downstream key visuals with the existing `站点管理 / 连接管理` style. -- Reuse the same site button interaction already present on the accounts page. -- Prefer direct reuse over inventing a new visual pattern. - -## Design - -### 1. Downstream key page visual alignment - -Keep the data model and operations unchanged, but reduce the visual mismatch: - -- Keep the existing `page-header`, `card`, and `data-table` structure. -- Restyle summary/filter regions to read like management controls rather than dashboard hero sections. -- Preserve the drawer/details flow, but make the main list view feel closer to `Sites.tsx` and `Accounts.tsx`. -- Replace repeated ad-hoc chip/button styling where possible with the same badge/link language already used elsewhere. - -This is intentionally a presentation cleanup, not a workflow redesign. - -### 2. Shared site button behavior - -Extract the accounts page site badge behavior into a reusable component: - -- Same hover affordance. -- Same visual style. -- Same navigation target: `/sites?focusSiteId=`. -- Graceful fallback when `siteId` is missing or invalid. - -Pages to update: - -- `src/web/pages/Models.tsx` -- `src/web/pages/ProxyLogs.tsx` -- `src/web/pages/TokenRoutes.tsx` -- `src/web/pages/DownstreamKeys.tsx` - -The `Sites.tsx` page itself continues to use direct external URL links, because those links serve a different purpose. - -### 3. Testing approach - -Cover the change with focused UI tests: - -- One shared component test for hover/click navigation behavior. -- Page-level regression tests where existing snapshots/assertions are already present. -- Keep tests narrow: validate that site labels render with the shared class/behavior and navigate to the focused site management route. - -## Risks - -- `TokenRoutes.tsx` contains multiple kinds of site labels; only labels intended to behave like the accounts page should be converted. -- `DownstreamKeys.tsx` may not currently expose enough site metadata in every row; if site links are only meaningful in detail regions, apply the shared component there instead of forcing low-value links into the table. -- Visual cleanup should not destabilize mobile layout or row actions. - -## Acceptance Criteria - -- The downstream key page looks consistent with the restrained management-console pages. -- Site entry points in models, proxy logs, token routes, and downstream keys match the accounts page interaction. -- Hover and navigation behavior are consistent. -- Existing relevant tests pass, and new tests cover the shared site link behavior. diff --git a/src/web/index.css b/src/web/index.css index 3151f7e..ab00a2d 100644 --- a/src/web/index.css +++ b/src/web/index.css @@ -1891,6 +1891,149 @@ body { outline: none; } +.downstream-key-modal-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.downstream-key-modal-field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.downstream-key-modal-label { + font-size: 12px; + line-height: 1.4; + font-weight: 500; + color: var(--color-text-muted); +} + +.downstream-key-modal-field-full { + width: 100%; +} + +.downstream-key-modal-toggle { + display: flex; + align-items: center; + gap: 10px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 10px 12px; + background: var(--color-bg); + cursor: pointer; + min-height: 42px; +} + +.downstream-key-modal-toggle-title { + font-size: 13px; + line-height: 1.35; + font-weight: 600; + color: var(--color-text-primary); +} + +.downstream-key-modal-help { + font-size: 12px; + line-height: 1.55; + color: var(--color-text-muted); +} + +.downstream-key-modal-meta { + font-size: 12px; + line-height: 1.45; + color: var(--color-text-secondary); +} + +.downstream-key-modal-section-title { + font-size: 13px; + line-height: 1.35; + font-weight: 600; + color: var(--color-text-primary); +} + +.downstream-key-advanced { + display: flex; + flex-direction: column; + gap: 10px; +} + +.downstream-key-advanced-toggle { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-bg); + color: var(--color-text-primary); + cursor: pointer; + font-size: 13px; + font-weight: 600; + line-height: 1.35; +} + +.downstream-key-advanced-toggle:hover { + border-color: color-mix(in srgb, var(--color-primary) 35%, var(--color-border)); + background: color-mix(in srgb, var(--color-primary) 5%, var(--color-bg)); +} + +.downstream-key-advanced-content { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; + border: 1px solid var(--color-border-light); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--color-bg) 60%, var(--color-bg-card)); +} + +.downstream-key-advanced-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.downstream-key-advanced-panel { + border: 1px solid var(--color-border-light); + border-radius: var(--radius-md); + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; + background: var(--color-bg-card); +} + +.downstream-key-modal-grid input, +.downstream-key-modal-grid textarea, +.downstream-key-advanced input, +.downstream-key-advanced textarea { + font-size: 13px; +} + +.downstream-key-modal-grid input::placeholder, +.downstream-key-modal-grid textarea::placeholder, +.downstream-key-advanced input::placeholder, +.downstream-key-advanced textarea::placeholder { + font-size: 13px; + color: color-mix(in srgb, var(--color-text-muted) 92%, white); +} + +.downstream-key-modal-grid textarea, +.downstream-key-advanced textarea { + line-height: 1.5; +} + +@media (max-width: 900px) { + .downstream-key-modal-grid, + .downstream-key-advanced-grid { + grid-template-columns: 1fr; + } +} + .route-enable-toggle { border: 1px solid transparent; transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; @@ -4724,4 +4867,4 @@ textarea { .route-card-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} diff --git a/src/web/pages/DownstreamKeys.test.tsx b/src/web/pages/DownstreamKeys.test.tsx index af8153b..c5730d6 100644 --- a/src/web/pages/DownstreamKeys.test.tsx +++ b/src/web/pages/DownstreamKeys.test.tsx @@ -132,7 +132,7 @@ beforeEach(() => { apiMock.getDownstreamApiKeysSummary.mockResolvedValue({ success: true, items: [buildSummaryItem()] }); apiMock.getDownstreamApiKeys.mockResolvedValue({ success: true, items: [buildRawItem()] }); apiMock.getRoutesLite.mockResolvedValue([ - { id: 11, modelPattern: 'team/default', displayName: '默认群组', enabled: true }, + { id: 11, modelPattern: 'claude-*', displayName: '默认群组', enabled: true }, { id: 12, modelPattern: 'gpt-4.1-mini', displayName: 'GPT 4.1 Mini', enabled: true }, ]); apiMock.getDownstreamApiKeyOverview.mockResolvedValue({ @@ -184,6 +184,8 @@ describe('DownstreamKeys page', () => { const text = collectText(root!.root); expect(text).toContain('下游密钥'); + expect(text).toContain('范围概览'); + expect(text).toContain('筛选与列表'); expect(text).toContain('smoke-key'); expect(text).toContain('sk-s****0315'); expect(text).toContain('默认群组'); @@ -264,9 +266,14 @@ describe('DownstreamKeys page', () => { }); await flushMicrotasks(); + expect(collectText(root!.root)).toContain('高级配置'); + expect(collectText(root!.root)).not.toContain('站点倍率 JSON'); + const inputs = root!.root.findAllByType('input'); + const tagInput = inputs.find((node) => node.props.placeholder === '输入标签后按回车或逗号,例如:移动端、VIP、项目A'); const nameInput = inputs.find((node) => node.props.placeholder === '例如:项目 A / 移动端'); const keyInput = inputs.find((node) => node.props.placeholder === 'sk-...'); + expect(tagInput?.props.style?.fontSize).toBe(13); await act(async () => { nameInput!.props.onChange({ target: { value: 'new-key' } }); keyInput!.props.onChange({ target: { value: 'sk-new-key-0315' } }); @@ -310,6 +317,50 @@ describe('DownstreamKeys page', () => { } }); + it('separates exact models from group routes in advanced config and uses single-column layout', async () => { + let root: ReturnType | null = null; + try { + await act(async () => { + root = create( + + + + + , + ); + }); + await flushMicrotasks(); + + const createBtn = root!.root.findAll((node) => node.type === 'button' && collectText(node).includes('新增下游密钥'))[0]; + await act(async () => { + createBtn.props.onClick(); + }); + await flushMicrotasks(); + + const advancedBtn = root!.root.findAll((node) => node.type === 'button' && collectText(node).includes('高级配置'))[0]; + await act(async () => { + advancedBtn.props.onClick(); + }); + await flushMicrotasks(); + + const panels = root!.root.findAll((node) => node.props.className === 'downstream-key-advanced-panel'); + const modelPanel = panels.find((node) => collectText(node).includes('模型白名单')); + const groupPanel = panels.find((node) => collectText(node).includes('群组范围')); + const advancedGrid = root!.root.findAll((node) => node.props.className === 'downstream-key-advanced-grid')[0]; + + expect(modelPanel).toBeTruthy(); + expect(groupPanel).toBeTruthy(); + expect(collectText(modelPanel!)).toContain('gpt-4.1-mini'); + expect(collectText(modelPanel!)).not.toContain('默认群组'); + expect(collectText(modelPanel!)).not.toContain('claude-*'); + expect(collectText(groupPanel!)).toContain('默认群组'); + expect(collectText(groupPanel!)).not.toContain('GPT 4.1 Mini'); + expect(advancedGrid.props.style?.gridTemplateColumns).toBe('1fr'); + } finally { + root?.unmount(); + } + }); + it('uses backend batch api for selected rows', async () => { let root: ReturnType | null = null; try { @@ -329,6 +380,7 @@ describe('DownstreamKeys page', () => { checkbox!.props.onChange({ target: { checked: true } }); }); await flushMicrotasks(); + expect(collectText(root!.root)).toContain('已选 1 个密钥'); const batchButton = root!.root.findAll((node) => node.type === 'button' && collectText(node).includes('批量启用'))[0]; await act(async () => { @@ -365,6 +417,12 @@ describe('DownstreamKeys page', () => { }); await flushMicrotasks(); + const advancedBtn = root!.root.findAll((node) => node.type === 'button' && collectText(node).includes('高级配置'))[0]; + await act(async () => { + advancedBtn.props.onClick(); + }); + await flushMicrotasks(); + const inputs = root!.root.findAllByType('input'); const nameInput = inputs.find((node) => node.props.placeholder === '例如:项目 A / 移动端'); const keyInput = inputs.find((node) => node.props.placeholder === 'sk-...'); diff --git a/src/web/pages/DownstreamKeys.tsx b/src/web/pages/DownstreamKeys.tsx index 13cd2a0..36ac5d6 100644 --- a/src/web/pages/DownstreamKeys.tsx +++ b/src/web/pages/DownstreamKeys.tsx @@ -170,6 +170,10 @@ function routeTitle(route: RouteSelectorItem): string { return displayName || route.modelPattern; } +function isGroupRouteOption(route: RouteSelectorItem): boolean { + return !isExactModelPattern(route.modelPattern); +} + function uniqStrings(values: string[]): string[] { return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))]; } @@ -312,7 +316,7 @@ function TagChips({ maxVisible?: number; }) { if (!Array.isArray(tags) || tags.length === 0) { - return 无标签; + return 无标签; } const visible = tags.slice(0, maxVisible); @@ -320,11 +324,15 @@ function TagChips({ return (
{visible.map((tag) => ( - + {tag} ))} - {hidden > 0 ? {`+${hidden}`} : null} + {hidden > 0 ? {`+${hidden}`} : null}
); } @@ -391,7 +399,7 @@ function TagInput({ } }} placeholder={placeholder || '输入标签后按回车或逗号'} - style={{ width: '100%', border: 'none', outline: 'none', background: 'transparent', color: 'var(--color-text-primary)', padding: 0 }} + style={{ width: '100%', border: 'none', outline: 'none', background: 'transparent', color: 'var(--color-text-primary)', padding: 0, fontSize: 13, lineHeight: 1.45 }} /> {suggestionPool.length > 0 ? ( @@ -421,9 +429,9 @@ function SummaryMetric({ value: string; }) { return ( -
+
{label} - {value} + {value}
); } @@ -440,15 +448,7 @@ function resolveOverviewUsageByRange( function TrendChartFallback({ height = 260 }: { height?: number }) { return ( -
+
@@ -542,29 +542,7 @@ function RangeToggle({ range, onChange }: { range: Range; onChange: (r: Range) = function StatusBadge({ enabled }: { enabled: boolean }) { return ( - - + {enabled ? '启用' : '禁用'} ); @@ -660,7 +638,7 @@ function Drawer({ animation: presence.isVisible ? 'drawer-slide-in 0.3s cubic-bezier(0.22, 1, 0.36, 1) both' : 'drawer-slide-out 0.22s cubic-bezier(0.4, 0, 1, 1) both', }} > -
+
{item?.name || '--'} @@ -670,7 +648,7 @@ function Drawer({ {item?.keyMasked || '****'}
- + {item?.groupName ? `主分组 · ${item.groupName}` : '未分组'} @@ -682,22 +660,23 @@ function Drawer({
-
-
- 趋势 +
+
+
+
使用趋势
+
按选定时间窗口查看请求、Tokens 与成本变化。
+
+
- + + }> + +
- }> - - - -
- -
-
- 概览 +
+
+ 基础信息
{overviewLoading ? (
@@ -731,10 +710,8 @@ function Drawer({ )}
-
-
-
+
当前范围汇总
@@ -759,9 +736,8 @@ function Drawer({ {overview?.usage ? ( <> -
-
-
+
+
固定窗口对比
@@ -817,11 +793,13 @@ function EditorModal({ }) { const [modelSearch, setModelSearch] = useState(''); const [groupSearch, setGroupSearch] = useState(''); + const [advancedOpen, setAdvancedOpen] = useState(false); useEffect(() => { if (!open) { setModelSearch(''); setGroupSearch(''); + setAdvancedOpen(false); } }, [open]); @@ -829,6 +807,18 @@ function EditorModal({ () => uniqStrings(routeOptions.filter((item) => isExactModelPattern(item.modelPattern)).map((item) => item.modelPattern)).sort((a, b) => a.localeCompare(b)), [routeOptions], ); + const groupRouteOptions = useMemo( + () => routeOptions.filter(isGroupRouteOption), + [routeOptions], + ); + const validGroupRouteIdSet = useMemo( + () => new Set(groupRouteOptions.map((route) => route.id)), + [groupRouteOptions], + ); + const normalizedSelectedGroupRouteIds = useMemo( + () => uniqIds(form.selectedGroupRouteIds.filter((id) => validGroupRouteIdSet.has(id))), + [form.selectedGroupRouteIds, validGroupRouteIdSet], + ); const filteredModels = useMemo(() => { const keyword = modelSearch.trim().toLowerCase(); @@ -838,15 +828,15 @@ function EditorModal({ const filteredGroups = useMemo(() => { const keyword = groupSearch.trim().toLowerCase(); - if (!keyword) return routeOptions; - return routeOptions.filter((route) => { + if (!keyword) return groupRouteOptions; + return groupRouteOptions.filter((route) => { const title = routeTitle(route).toLowerCase(); return title.includes(keyword) || route.modelPattern.toLowerCase().includes(keyword); }); - }, [groupSearch, routeOptions]); + }, [groupRouteOptions, groupSearch]); const selectedModelCount = form.selectedModels.length; - const selectedGroupCount = form.selectedGroupRouteIds.length; + const selectedGroupCount = normalizedSelectedGroupRouteIds.length; const inputStyle: React.CSSProperties = { width: '100%', padding: '10px 12px', @@ -854,6 +844,8 @@ function EditorModal({ borderRadius: 'var(--radius-sm)', background: 'var(--color-bg)', color: 'var(--color-text-primary)', + fontSize: 13, + lineHeight: 1.45, }; return ( @@ -861,7 +853,7 @@ function EditorModal({ open={open} onClose={onClose} title={editingItem ? '编辑下游密钥' : '新增下游密钥'} - maxWidth={1040} + maxWidth={860} bodyStyle={{ display: 'flex', flexDirection: 'column', gap: 12 }} footer={( <> @@ -875,20 +867,20 @@ function EditorModal({ )} >
- 支持为每个下游密钥独立配置主分组、标签、过期时间、额度上限、模型白名单与群组范围。留空表示不限制。 + 支持为每个下游密钥独立配置分组、标签、额度与有效期。高级限制项可按需展开。
-
-
-
名称
+
+
+
名称
onChange((prev) => ({ ...prev, name: e.target.value }))} placeholder="例如:项目 A / 移动端" style={inputStyle} />
-
-
下游密钥
+
+
下游密钥
onChange((prev) => ({ ...prev, key: e.target.value }))} placeholder="sk-..." style={{ ...inputStyle, fontFamily: 'var(--font-mono)' }} />
-
-
主分组
+
+
主分组
onChange((prev) => ({ ...prev, groupName: e.target.value }))} @@ -897,42 +889,31 @@ function EditorModal({ style={inputStyle} />
-
-
请求额度
+
+
请求额度
onChange((prev) => ({ ...prev, maxRequests: e.target.value }))} placeholder="留空表示不限" style={inputStyle} />
-
-
成本额度
+
+
成本额度
onChange((prev) => ({ ...prev, maxCost: e.target.value }))} placeholder="留空表示不限" style={inputStyle} />
-
-
过期时间
+
+
过期时间
onChange((prev) => ({ ...prev, expiresAt: e.target.value }))} style={inputStyle} />
-
-
备注说明
+
+
备注说明