From d9e33e71ed6cd663722d6d07e604ddb90db23017 Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Mon, 16 Mar 2026 14:48:46 +0800 Subject: [PATCH] Unify site badge links across console pages --- src/web/components/SiteBadgeLink.tsx | 37 ++++++++++++++++++ src/web/components/site-badge-link.test.tsx | 34 ++++++++++++++++ src/web/pages/Accounts.tsx | 43 ++++++--------------- src/web/pages/Models.tsx | 21 ++++++++-- src/web/pages/ProxyLogs.tsx | 15 ++++++- 5 files changed, 113 insertions(+), 37 deletions(-) create mode 100644 src/web/components/SiteBadgeLink.tsx create mode 100644 src/web/components/site-badge-link.test.tsx diff --git a/src/web/components/SiteBadgeLink.tsx b/src/web/components/SiteBadgeLink.tsx new file mode 100644 index 0000000..c18bc1b --- /dev/null +++ b/src/web/components/SiteBadgeLink.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +type SiteBadgeLinkProps = { + siteId?: number | null; + siteName?: string | null; + className?: string; + badgeClassName?: string; + badgeStyle?: React.CSSProperties; +}; + +export default function SiteBadgeLink({ + siteId, + siteName, + className = 'badge-link', + badgeClassName = 'badge badge-muted', + badgeStyle, +}: SiteBadgeLinkProps) { + const label = String(siteName || '').trim() || '-'; + const normalizedSiteId = Number(siteId); + + if (!Number.isFinite(normalizedSiteId) || normalizedSiteId <= 0) { + return ( + + {label} + + ); + } + + return ( + + + {label} + + + ); +} diff --git a/src/web/components/site-badge-link.test.tsx b/src/web/components/site-badge-link.test.tsx new file mode 100644 index 0000000..b01b69a --- /dev/null +++ b/src/web/components/site-badge-link.test.tsx @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { create } from 'react-test-renderer'; +import { MemoryRouter } from 'react-router-dom'; +import SiteBadgeLink from './SiteBadgeLink.js'; + +describe('SiteBadgeLink', () => { + it('renders a focus-navigation link when site id is valid', () => { + const root = create( + + + , + ); + + const link = root.root.findByType('a'); + expect(String(link.props.href || '')).toContain('/sites?focusSiteId=7'); + expect(String(link.props.className || '')).toContain('badge-link'); + expect(root.root.findByProps({ className: 'badge badge-muted' }).children.join('')).toContain('Demo Site'); + + root.unmount(); + }); + + it('falls back to plain badge text when site id is invalid', () => { + const root = create( + + + , + ); + + expect(root.root.findAllByType('a')).toHaveLength(0); + expect(root.root.findByProps({ className: 'badge badge-muted' }).children.join('')).toContain('Unknown Site'); + + root.unmount(); + }); +}); diff --git a/src/web/pages/Accounts.tsx b/src/web/pages/Accounts.tsx index 1659ddd..3a9bd94 100644 --- a/src/web/pages/Accounts.tsx +++ b/src/web/pages/Accounts.tsx @@ -7,6 +7,7 @@ import ModernSelect from '../components/ModernSelect.js'; import { MobileCard, MobileField } from '../components/MobileCard.js'; import { useIsMobile } from '../components/useIsMobile.js'; import DeleteConfirmModal from '../components/DeleteConfirmModal.js'; +import SiteBadgeLink from '../components/SiteBadgeLink.js'; import { buildAddAccountPrereqHint, buildVerifyFailureHint, @@ -1634,21 +1635,12 @@ export default function Accounts() {
- - {a.site?.name || '-'} - - - ) : ( - - {a.site?.name || '-'} - + value={( + )} /> - {a.site?.url ? ( - - - {a.site?.name || '-'} - - - ) : ( - - {a.site?.name || '-'} - - )} + {(() => { diff --git a/src/web/pages/Models.tsx b/src/web/pages/Models.tsx index 38d4fe1..da6ecf5 100644 --- a/src/web/pages/Models.tsx +++ b/src/web/pages/Models.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useLocation } from 'react-router-dom'; import { api } from '../api.js'; import { BrandGlyph, getBrand, hashColor, BrandIcon, type BrandInfo } from '../components/BrandIcon.js'; +import SiteBadgeLink from '../components/SiteBadgeLink.js'; import { useToast } from '../components/Toast.js'; import ModernSelect from '../components/ModernSelect.js'; import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js'; @@ -168,6 +169,18 @@ export default function Models() { const latestPrimaryRequestRef = useRef(0); const latestMetadataRequestRef = useRef(0); const location = useLocation(); + const siteIdByName = useMemo(() => { + const index = new Map(); + for (const model of data.models) { + for (const source of model.pricingSources || []) { + const siteName = String(source.siteName || '').trim(); + const siteId = Number(source.siteId); + if (!siteName || !Number.isFinite(siteId) || siteId <= 0 || index.has(siteName)) continue; + index.set(siteName, Math.trunc(siteId)); + } + } + return index; + }, [data.models]); const loadBaseMarketplace = useCallback(async (refresh = false) => { const requestId = ++latestPrimaryRequestRef.current; @@ -688,7 +701,7 @@ export default function Models() { style={{ border: '1px solid var(--color-border-light)', borderRadius: 8, padding: 8 }} >
- {source.siteName} · {source.username || `ID:${source.accountId}`} + · {source.username || `ID:${source.accountId}`}
{Object.entries(source.groupPricing).map(([group, pricing]) => ( @@ -719,7 +732,7 @@ export default function Models() { {m.accounts.map(a => ( - {a.site} + {a.username || `ID:${a.id}`} {a.tokens.length > 0 ? a.tokens.map(t => ( @@ -849,7 +862,7 @@ export default function Models() { style={{ border: '1px solid var(--color-border-light)', borderRadius: 8, padding: 8 }} >
- {source.siteName} · {source.username || `ID:${source.accountId}`} + · {source.username || `ID:${source.accountId}`}
{Object.entries(source.groupPricing).map(([group, pricing]) => ( @@ -878,7 +891,7 @@ export default function Models() { {m.accounts.map(a => ( - {a.site} + {a.username || `ID:${a.id}`} {a.tokens.length > 0 ? a.tokens.map(t => ( diff --git a/src/web/pages/ProxyLogs.tsx b/src/web/pages/ProxyLogs.tsx index 3025400..1931e62 100644 --- a/src/web/pages/ProxyLogs.tsx +++ b/src/web/pages/ProxyLogs.tsx @@ -10,6 +10,7 @@ import { } from '../api.js'; import { useToast } from '../components/Toast.js'; import { ModelBadge } from '../components/BrandIcon.js'; +import SiteBadgeLink from '../components/SiteBadgeLink.js'; import { MobileCard, MobileField } from '../components/MobileCard.js'; import { MobileDrawer } from '../components/MobileDrawer.js'; import { useIsMobile } from '../components/useIsMobile.js'; @@ -329,6 +330,16 @@ export default function ProxyLogs() { if (!siteFilter) return '全部站点'; return siteOptions.find((option) => option.value === String(siteFilter))?.label || `站点 #${siteFilter}`; }, [siteFilter, siteOptions]); + const siteIdByName = useMemo(() => { + const index = new Map(); + for (const site of sites) { + const siteName = String(site?.name || '').trim(); + const siteId = Number(site?.id); + if (!siteName || !Number.isFinite(siteId) || siteId <= 0 || index.has(siteName)) continue; + index.set(siteName, Math.trunc(siteId)); + } + return index; + }, [sites]); const load = useCallback(async (silent = false) => { const seq = ++loadSeq.current; @@ -608,7 +619,7 @@ export default function ProxyLogs() { )} > - + } /> {downstreamKeySummary ? : null} @@ -709,7 +720,7 @@ export default function ProxyLogs() {
- {log.siteName || '-'} +