mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8232812ac2 | ||
|
|
2ae06a8860 | ||
|
|
dc58a0701f |
@@ -27,6 +27,8 @@
|
|||||||
|
|
||||||
&--animating &__layer {
|
&--animating &__layer {
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-style: preserve-3d;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When both layers exist, current layer also needs positioning
|
// When both layers exist, current layer also needs positioning
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ interface PageTransitionProps {
|
|||||||
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
scrollContainerRef?: React.RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSITION_DURATION = 0.65;
|
const TRANSITION_DURATION = 0.5;
|
||||||
|
const EXIT_DURATION = 0.45;
|
||||||
|
const ENTER_DELAY = 0.08;
|
||||||
|
|
||||||
type LayerStatus = 'current' | 'exiting';
|
type LayerStatus = 'current' | 'exiting';
|
||||||
|
|
||||||
@@ -99,6 +101,13 @@ export function PageTransition({
|
|||||||
scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerHeight = scrollContainer?.clientHeight ?? 0;
|
||||||
|
const viewportHeight = typeof window === 'undefined' ? 0 : window.innerHeight;
|
||||||
|
const travelDistance = Math.max(containerHeight, viewportHeight, 1);
|
||||||
|
const enterFromY = transitionDirection === 'forward' ? travelDistance : -travelDistance;
|
||||||
|
const exitToY = transitionDirection === 'forward' ? -travelDistance : travelDistance;
|
||||||
|
const exitBaseY = scrollOffset ? -scrollOffset : 0;
|
||||||
|
|
||||||
const tl = gsap.timeline({
|
const tl = gsap.timeline({
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
setLayers((prev) => prev.filter((layer) => layer.status !== 'exiting'));
|
setLayers((prev) => prev.filter((layer) => layer.status !== 'exiting'));
|
||||||
@@ -108,15 +117,16 @@ export function PageTransition({
|
|||||||
|
|
||||||
// Exit animation: fly out to top (slow-to-fast)
|
// Exit animation: fly out to top (slow-to-fast)
|
||||||
if (exitingLayerRef.current) {
|
if (exitingLayerRef.current) {
|
||||||
gsap.set(exitingLayerRef.current, { y: scrollOffset ? -scrollOffset : 0 });
|
gsap.set(exitingLayerRef.current, { y: exitBaseY });
|
||||||
tl.fromTo(
|
tl.fromTo(
|
||||||
exitingLayerRef.current,
|
exitingLayerRef.current,
|
||||||
{ yPercent: 0, opacity: 1 },
|
{ y: exitBaseY, opacity: 1 },
|
||||||
{
|
{
|
||||||
yPercent: transitionDirection === 'forward' ? -100 : 100,
|
y: exitBaseY + exitToY,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: TRANSITION_DURATION,
|
duration: EXIT_DURATION,
|
||||||
ease: 'power3.in', // slow start, fast end
|
ease: 'power2.in', // fast finish to clear screen
|
||||||
|
force3D: true,
|
||||||
},
|
},
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
@@ -125,15 +135,16 @@ export function PageTransition({
|
|||||||
// Enter animation: slide in from bottom (slow-to-fast)
|
// Enter animation: slide in from bottom (slow-to-fast)
|
||||||
tl.fromTo(
|
tl.fromTo(
|
||||||
currentLayerRef.current,
|
currentLayerRef.current,
|
||||||
{ yPercent: transitionDirection === 'forward' ? 100 : -100, opacity: 0 },
|
{ y: enterFromY, opacity: 0 },
|
||||||
{
|
{
|
||||||
yPercent: 0,
|
y: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
duration: TRANSITION_DURATION,
|
duration: TRANSITION_DURATION,
|
||||||
ease: 'power2.in', // slow start, fast end
|
ease: 'power2.out', // smooth settle
|
||||||
clearProps: 'transform,opacity',
|
clearProps: 'transform,opacity',
|
||||||
|
force3D: true,
|
||||||
},
|
},
|
||||||
0
|
ENTER_DELAY
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -719,9 +719,11 @@ export function AuthFilesPage() {
|
|||||||
const renderFileCard = (item: AuthFileItem) => {
|
const renderFileCard = (item: AuthFileItem) => {
|
||||||
const fileStats = resolveAuthFileStats(item, keyStats);
|
const fileStats = resolveAuthFileStats(item, keyStats);
|
||||||
const isRuntimeOnly = isRuntimeOnlyAuthFile(item);
|
const isRuntimeOnly = isRuntimeOnlyAuthFile(item);
|
||||||
|
const isAistudio = (item.type || '').toLowerCase() === 'aistudio';
|
||||||
|
const showModelsButton = !isRuntimeOnly || isAistudio;
|
||||||
const typeColor = getTypeColor(item.type || 'unknown');
|
const typeColor = getTypeColor(item.type || 'unknown');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.name} className={styles.fileCard}>
|
<div key={item.name} className={styles.fileCard}>
|
||||||
<div className={styles.cardHeader}>
|
<div className={styles.cardHeader}>
|
||||||
<span
|
<span
|
||||||
@@ -753,29 +755,29 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
{/* 状态监测栏 */}
|
{/* 状态监测栏 */}
|
||||||
{renderStatusBar(item)}
|
{renderStatusBar(item)}
|
||||||
|
|
||||||
<div className={styles.cardActions}>
|
<div className={styles.cardActions}>
|
||||||
{isRuntimeOnly ? (
|
{showModelsButton && (
|
||||||
<div className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</div>
|
<Button
|
||||||
) : (
|
variant="secondary"
|
||||||
<>
|
size="sm"
|
||||||
<Button
|
onClick={() => showModels(item)}
|
||||||
variant="secondary"
|
className={styles.iconButton}
|
||||||
size="sm"
|
title={t('auth_files.models_button', { defaultValue: '模型' })}
|
||||||
onClick={() => showModels(item)}
|
disabled={disableControls}
|
||||||
className={styles.iconButton}
|
>
|
||||||
title={t('auth_files.models_button', { defaultValue: '模型' })}
|
<IconBot className={styles.actionIcon} size={16} />
|
||||||
disabled={disableControls}
|
</Button>
|
||||||
>
|
)}
|
||||||
<IconBot className={styles.actionIcon} size={16} />
|
{!isRuntimeOnly && (
|
||||||
</Button>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => showDetails(item)}
|
onClick={() => showDetails(item)}
|
||||||
className={styles.iconButton}
|
className={styles.iconButton}
|
||||||
title={t('common.info', { defaultValue: '关于' })}
|
title={t('common.info', { defaultValue: '关于' })}
|
||||||
disabled={disableControls}
|
disabled={disableControls}
|
||||||
>
|
>
|
||||||
<IconInfo className={styles.actionIcon} size={16} />
|
<IconInfo className={styles.actionIcon} size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -799,13 +801,16 @@ export function AuthFilesPage() {
|
|||||||
>
|
>
|
||||||
{deleting === item.name ? (
|
{deleting === item.name ? (
|
||||||
<LoadingSpinner size={14} />
|
<LoadingSpinner size={14} />
|
||||||
) : (
|
) : (
|
||||||
<IconTrash2 className={styles.actionIcon} size={16} />
|
<IconTrash2 className={styles.actionIcon} size={16} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
{isRuntimeOnly && (
|
||||||
|
<div className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ const HTTP_METHOD_REGEX = new RegExp(`\\b(${HTTP_METHODS.join('|')})\\b`);
|
|||||||
const LOG_TIMESTAMP_REGEX = /^\[?(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\]?/;
|
const LOG_TIMESTAMP_REGEX = /^\[?(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\]?/;
|
||||||
const LOG_LEVEL_REGEX = /^\[?(trace|debug|info|warn|warning|error|fatal)\s*\]?(?=\s|\[|$)\s*/i;
|
const LOG_LEVEL_REGEX = /^\[?(trace|debug|info|warn|warning|error|fatal)\s*\]?(?=\s|\[|$)\s*/i;
|
||||||
const LOG_SOURCE_REGEX = /^\[([^\]]+)\]/;
|
const LOG_SOURCE_REGEX = /^\[([^\]]+)\]/;
|
||||||
const LOG_LATENCY_REGEX = /\b(\d+(?:\.\d+)?)(?:\s*)(µs|us|ms|s)\b/i;
|
const LOG_LATENCY_REGEX =
|
||||||
|
/\b(?:\d+(?:\.\d+)?\s*(?:µs|us|ms|s|m))(?:\s*\d+(?:\.\d+)?\s*(?:µs|us|ms|s|m))*\b/i;
|
||||||
const LOG_IPV4_REGEX = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
|
const LOG_IPV4_REGEX = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
|
||||||
const LOG_IPV6_REGEX = /\b(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}\b/i;
|
const LOG_IPV6_REGEX = /\b(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}\b/i;
|
||||||
const LOG_REQUEST_ID_REGEX = /^([a-f0-9]{8}|--------)$/i;
|
const LOG_REQUEST_ID_REGEX = /^([a-f0-9]{8}|--------)$/i;
|
||||||
@@ -102,6 +103,12 @@ const normalizeTimestampToSeconds = (value: string): string => {
|
|||||||
return `${match[1]} ${match[2]}`;
|
return `${match[1]} ${match[2]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractLatency = (text: string): string | undefined => {
|
||||||
|
const match = text.match(LOG_LATENCY_REGEX);
|
||||||
|
if (!match) return undefined;
|
||||||
|
return match[0].replace(/\s+/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
type ParsedLogLine = {
|
type ParsedLogLine = {
|
||||||
raw: string;
|
raw: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
@@ -244,9 +251,9 @@ const parseLogLine = (raw: string): ParsedLogLine => {
|
|||||||
// latency
|
// latency
|
||||||
const latencyIndex = segments.findIndex((segment) => LOG_LATENCY_REGEX.test(segment));
|
const latencyIndex = segments.findIndex((segment) => LOG_LATENCY_REGEX.test(segment));
|
||||||
if (latencyIndex >= 0) {
|
if (latencyIndex >= 0) {
|
||||||
const match = segments[latencyIndex].match(LOG_LATENCY_REGEX);
|
const extracted = extractLatency(segments[latencyIndex]);
|
||||||
if (match) {
|
if (extracted) {
|
||||||
latency = `${match[1]}${match[2]}`;
|
latency = extracted;
|
||||||
consumed.add(latencyIndex);
|
consumed.add(latencyIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,8 +294,8 @@ const parseLogLine = (raw: string): ParsedLogLine => {
|
|||||||
} else {
|
} else {
|
||||||
statusCode = detectHttpStatusCode(remaining);
|
statusCode = detectHttpStatusCode(remaining);
|
||||||
|
|
||||||
const latencyMatch = remaining.match(LOG_LATENCY_REGEX);
|
const extracted = extractLatency(remaining);
|
||||||
if (latencyMatch) latency = `${latencyMatch[1]}${latencyMatch[2]}`;
|
if (extracted) latency = extracted;
|
||||||
|
|
||||||
ip = extractIp(remaining);
|
ip = extractIp(remaining);
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,13 @@
|
|||||||
margin-top: $spacing-sm;
|
margin-top: $spacing-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.geminiProjectField {
|
||||||
|
:global(.form-group) {
|
||||||
|
margin-top: $spacing-sm;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filePicker {
|
.filePicker {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -327,19 +327,21 @@ export function OAuthPage() {
|
|||||||
>
|
>
|
||||||
<div className="hint">{t(provider.hintKey)}</div>
|
<div className="hint">{t(provider.hintKey)}</div>
|
||||||
{provider.id === 'gemini-cli' && (
|
{provider.id === 'gemini-cli' && (
|
||||||
<Input
|
<div className={styles.geminiProjectField}>
|
||||||
label={t('auth_login.gemini_cli_project_id_label')}
|
<Input
|
||||||
hint={t('auth_login.gemini_cli_project_id_hint')}
|
label={t('auth_login.gemini_cli_project_id_label')}
|
||||||
value={state.projectId || ''}
|
hint={t('auth_login.gemini_cli_project_id_hint')}
|
||||||
error={state.projectIdError}
|
value={state.projectId || ''}
|
||||||
onChange={(e) =>
|
error={state.projectIdError}
|
||||||
updateProviderState(provider.id, {
|
onChange={(e) =>
|
||||||
projectId: e.target.value,
|
updateProviderState(provider.id, {
|
||||||
projectIdError: undefined
|
projectId: e.target.value,
|
||||||
})
|
projectIdError: undefined
|
||||||
}
|
})
|
||||||
placeholder={t('auth_login.gemini_cli_project_id_placeholder')}
|
}
|
||||||
/>
|
placeholder={t('auth_login.gemini_cli_project_id_placeholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.url && (
|
{state.url && (
|
||||||
<div className={`connection-box ${styles.authUrlBox}`}>
|
<div className={`connection-box ${styles.authUrlBox}`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user