feat: add comprehensive tests for token candidates and proxy log path metadata

- Implemented tests for the `/api/models/token-candidates` endpoint to verify model availability and token coverage scenarios.
- Added tests for `composeProxyLogMessage` to ensure proper formatting of log messages with downstream and upstream metadata.
- Created utility functions for parsing proxy log path metadata and handling missing token hints.
- Developed tests for the `Sites` and `TokenRoutes` components to validate UI behavior and interactions, including scrolling and group expansion.
- Introduced a custom hook `useAnimatedVisibility` for managing visibility animations in components.
- Enhanced the documentation link for site resources.
This commit is contained in:
cita
2026-03-04 13:46:31 +08:00
parent d49fcefa05
commit e281e45e84
71 changed files with 5435 additions and 636 deletions
+5 -3
View File
@@ -3,8 +3,10 @@ import { createPortal } from 'react-dom';
import { api } from '../api.js';
import { useToast } from './Toast.js';
import { persistAuthSession } from '../authSession.js';
import { useAnimatedVisibility } from './useAnimatedVisibility.js';
export default function ChangeKeyModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const presence = useAnimatedVisibility(open, 200);
const [oldToken, setOldToken] = useState('');
const [newToken, setNewToken] = useState('');
const [confirmToken, setConfirmToken] = useState('');
@@ -56,7 +58,7 @@ export default function ChangeKeyModal({ open, onClose }: { open: boolean; onClo
}
};
if (!open) return null;
if (!presence.shouldRender) return null;
const inputStyle: React.CSSProperties = {
width: '100%', padding: '10px 14px', border: '1px solid var(--color-border)',
@@ -65,8 +67,8 @@ export default function ChangeKeyModal({ open, onClose }: { open: boolean; onClo
};
const modal = (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: 420 }}>
<div className={`modal-backdrop ${presence.isVisible ? '' : 'is-closing'}`.trim()} onClick={onClose}>
<div className={`modal-content ${presence.isVisible ? '' : 'is-closing'}`.trim()} onClick={e => e.stopPropagation()} style={{ maxWidth: 420 }}>
<div className="modal-header">修改管理员 Token</div>
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
+8 -2
View File
@@ -4,6 +4,7 @@ import { api } from '../api.js';
import { formatDateTimeMinuteLocal } from '../pages/helpers/checkinLogTime.js';
import { buildEventNavigationPath } from '../pages/helpers/navigationFocus.js';
import { useI18n } from '../i18n.js';
import { useAnimatedVisibility } from './useAnimatedVisibility.js';
const levelColors: Record<string, string> = {
info: 'var(--color-info)',
@@ -31,6 +32,7 @@ export default function NotificationPanel({
onUnreadCountChange?: (count: number) => void;
}) {
const { t: tr } = useI18n();
const presence = useAnimatedVisibility(open, 160);
const [events, setEvents] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState<string>('');
@@ -77,10 +79,14 @@ export default function NotificationPanel({
onUnreadCountChange?.(0);
};
if (!open) return null;
if (!presence.shouldRender) return null;
return (
<div ref={panelRef} className="user-dropdown" style={{ right: 0, top: '100%', width: 360, maxHeight: 480, padding: 0, marginTop: 4 }}>
<div
ref={panelRef}
className={`user-dropdown ${presence.isVisible ? '' : 'is-closing'}`.trim()}
style={{ right: 0, top: '100%', width: 360, maxHeight: 480, padding: 0, marginTop: 4 }}
>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', borderBottom: '1px solid var(--color-border-light)' }}>
<span style={{ fontWeight: 600, fontSize: 14 }}>{tr('通知')}</span>
+5 -3
View File
@@ -4,6 +4,7 @@ import { api } from '../api.js';
import { formatDateLocal, formatDateTimeMinuteLocal } from '../pages/helpers/checkinLogTime.js';
import { buildAccountFocusPath, buildSiteFocusPath } from '../pages/helpers/navigationFocus.js';
import { useI18n } from '../i18n.js';
import { useAnimatedVisibility } from './useAnimatedVisibility.js';
interface SiteResult {
id: number;
@@ -52,6 +53,7 @@ interface SearchResult {
export default function SearchModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const { t } = useI18n();
const presence = useAnimatedVisibility(open, 180);
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult | null>(null);
const [loading, setLoading] = useState(false);
@@ -109,7 +111,7 @@ export default function SearchModal({ open, onClose }: { open: boolean; onClose:
return () => document.removeEventListener('keydown', handler);
}, [open, onClose]);
if (!open) return null;
if (!presence.shouldRender) return null;
const hasResults = results && (
results.models.length
@@ -120,8 +122,8 @@ export default function SearchModal({ open, onClose }: { open: boolean; onClose:
);
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-content" style={{ maxWidth: 560, padding: 0 }} onClick={e => e.stopPropagation()}>
<div className={`modal-backdrop ${presence.isVisible ? '' : 'is-closing'}`.trim()} onClick={onClose}>
<div className={`modal-content ${presence.isVisible ? '' : 'is-closing'}`.trim()} style={{ maxWidth: 560, padding: 0 }} onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '14px 16px', borderBottom: '1px solid var(--color-border-light)' }}>
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="var(--color-text-muted)">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -0,0 +1,29 @@
import { useEffect, useState } from 'react';
export function useAnimatedVisibility(visible: boolean, durationMs = 220) {
const [shouldRender, setShouldRender] = useState(visible);
const [isVisible, setIsVisible] = useState(visible);
useEffect(() => {
if (visible) {
setShouldRender(true);
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
const rafId = window.requestAnimationFrame(() => setIsVisible(true));
return () => window.cancelAnimationFrame(rafId);
}
setIsVisible(true);
return undefined;
}
setIsVisible(false);
if (durationMs <= 0) {
setShouldRender(false);
return undefined;
}
const timerId = globalThis.setTimeout(() => setShouldRender(false), durationMs);
return () => globalThis.clearTimeout(timerId);
}, [visible, durationMs]);
return { shouldRender, isVisible };
}