Unify site badge links across console pages
This commit is contained in:
@@ -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 (
|
||||
<span className={badgeClassName} style={badgeStyle}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={`/sites?focusSiteId=${Math.trunc(normalizedSiteId)}`} className={className}>
|
||||
<span className={badgeClassName} style={badgeStyle}>
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -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(
|
||||
<MemoryRouter>
|
||||
<SiteBadgeLink siteId={7} siteName="Demo Site" />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<SiteBadgeLink siteId={0} siteName="Unknown Site" />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(root.root.findAllByType('a')).toHaveLength(0);
|
||||
expect(root.root.findByProps({ className: 'badge badge-muted' }).children.join('')).toContain('Unknown Site');
|
||||
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
+12
-31
@@ -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() {
|
||||
<div className="mobile-card-extra">
|
||||
<MobileField
|
||||
label="站点"
|
||||
value={a.site?.url ? (
|
||||
<a
|
||||
href={a.site.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="badge-link"
|
||||
>
|
||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
||||
{a.site?.name || '-'}
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
||||
{a.site?.name || '-'}
|
||||
</span>
|
||||
value={(
|
||||
<SiteBadgeLink
|
||||
siteId={a.site?.id}
|
||||
siteName={a.site?.name}
|
||||
badgeStyle={{ fontSize: 11 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<MobileField
|
||||
@@ -1803,22 +1795,11 @@ export default function Accounts() {
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{a.site?.url ? (
|
||||
<a
|
||||
href={a.site.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="badge-link"
|
||||
>
|
||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
||||
{a.site?.name || '-'}
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="badge badge-muted" style={{ fontSize: 11 }}>
|
||||
{a.site?.name || '-'}
|
||||
</span>
|
||||
)}
|
||||
<SiteBadgeLink
|
||||
siteId={a.site?.id}
|
||||
siteName={a.site?.name}
|
||||
badgeStyle={{ fontSize: 11 }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{(() => {
|
||||
|
||||
@@ -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<string, number>();
|
||||
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 }}
|
||||
>
|
||||
<div style={{ fontSize: 12, marginBottom: 6 }}>
|
||||
<strong>{source.siteName}</strong> · {source.username || `ID:${source.accountId}`}
|
||||
<SiteBadgeLink siteId={source.siteId} siteName={source.siteName} badgeStyle={{ fontSize: 11 }} /> · {source.username || `ID:${source.accountId}`}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{Object.entries(source.groupPricing).map(([group, pricing]) => (
|
||||
@@ -719,7 +732,7 @@ export default function Models() {
|
||||
<tbody>
|
||||
{m.accounts.map(a => (
|
||||
<tr key={a.id}>
|
||||
<td><span className="badge badge-info">{a.site}</span></td>
|
||||
<td><SiteBadgeLink siteId={siteIdByName.get(a.site)} siteName={a.site} badgeClassName="badge badge-info" badgeStyle={{ fontSize: 11 }} /></td>
|
||||
<td style={{ fontSize: 12 }}>{a.username || `ID:${a.id}`}</td>
|
||||
<td style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{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 }}
|
||||
>
|
||||
<div style={{ fontSize: 12, marginBottom: 6 }}>
|
||||
<strong>{source.siteName}</strong> · {source.username || `ID:${source.accountId}`}
|
||||
<SiteBadgeLink siteId={source.siteId} siteName={source.siteName} badgeStyle={{ fontSize: 11 }} /> · {source.username || `ID:${source.accountId}`}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{Object.entries(source.groupPricing).map(([group, pricing]) => (
|
||||
@@ -878,7 +891,7 @@ export default function Models() {
|
||||
<tbody>
|
||||
{m.accounts.map(a => (
|
||||
<tr key={a.id} style={{ borderTop: '1px solid var(--color-border-light)' }}>
|
||||
<td style={{ padding: 8 }}>{a.site}</td>
|
||||
<td style={{ padding: 8 }}><SiteBadgeLink siteId={siteIdByName.get(a.site)} siteName={a.site} badgeClassName="badge badge-info" badgeStyle={{ fontSize: 11 }} /></td>
|
||||
<td style={{ padding: 8 }}>{a.username || `ID:${a.id}`}</td>
|
||||
<td style={{ padding: 8, display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{a.tokens.length > 0 ? a.tokens.map(t => (
|
||||
|
||||
@@ -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<string, number>();
|
||||
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() {
|
||||
)}
|
||||
>
|
||||
<MobileField label="时间" value={formatDateTimeLocal(log.createdAt)} />
|
||||
<MobileField label="站点" value={log.siteName || '-'} />
|
||||
<MobileField label="站点" value={<SiteBadgeLink siteId={siteIdByName.get(String(log.siteName || '').trim())} siteName={log.siteName} badgeStyle={{ fontSize: 11 }} />} />
|
||||
{downstreamKeySummary ? <MobileField label="下游 Key" value={downstreamKeySummary} /> : null}
|
||||
<MobileField label="用时" value={formatLatency(log.latencyMs)} />
|
||||
<MobileField label="输入" value={log.promptTokens?.toLocaleString() || '-'} />
|
||||
@@ -709,7 +720,7 @@ export default function ProxyLogs() {
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>
|
||||
{log.siteName || '-'}
|
||||
<SiteBadgeLink siteId={siteIdByName.get(String(log.siteName || '').trim())} siteName={log.siteName} badgeStyle={{ fontSize: 11 }} />
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${log.status === 'success' ? 'badge-success' : 'badge-error'}`} style={{ fontSize: 11, fontWeight: 600 }}>
|
||||
|
||||
Reference in New Issue
Block a user