mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Compare commits
7 Commits
+2
-1
@@ -1,7 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" translate="no" class="notranslate">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="google" content="notranslate" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20aria-hidden%3D%22true%22%20role%3D%22img%22%20class%3D%22iconify%20iconify--logos%22%20width%3D%2231.88%22%20height%3D%2232%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20viewBox%3D%220%200%20256%20257%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22IconifyId1813088fe1fbc01fb466%22%20x1%3D%22-.828%25%22%20x2%3D%2257.636%25%22%20y1%3D%227.652%25%22%20y2%3D%2278.411%25%22%3E%3Cstop%20offset%3D%220%25%22%20stop-color%3D%22%2341D1FF%22%3E%3C%2Fstop%3E%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%23BD34FE%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D%22IconifyId1813088fe1fbc01fb467%22%20x1%3D%2243.376%25%22%20x2%3D%2250.316%25%22%20y1%3D%222.242%25%22%20y2%3D%2289.03%25%22%3E%3Cstop%20offset%3D%220%25%22%20stop-color%3D%22%23FFEA83%22%3E%3C%2Fstop%3E%3Cstop%20offset%3D%228.333%25%22%20stop-color%3D%22%23FFDD35%22%3E%3C%2Fstop%3E%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%23FFA800%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cpath%20fill%3D%22url(%23IconifyId1813088fe1fbc01fb466)%22%20d%3D%22M255.153%2037.938L134.897%20252.976c-2.483%204.44-8.862%204.466-11.382.048L.875%2037.958c-2.746-4.814%201.371-10.646%206.827-9.67l120.385%2021.517a6.537%206.537%200%200%200%202.322-.004l117.867-21.483c5.438-.991%209.574%204.796%206.877%209.62Z%22%3E%3C%2Fpath%3E%3Cpath%20fill%3D%22url(%23IconifyId1813088fe1fbc01fb467)%22%20d%3D%22M185.432.063L96.44%2017.501a3.268%203.268%200%200%200-2.634%203.014l-5.474%2092.456a3.268%203.268%200%200%200%203.997%203.378l24.777-5.718c2.318-.535%204.413%201.507%203.936%203.838l-7.361%2036.047c-.495%202.426%201.782%204.5%204.151%203.78l15.304-4.649c2.372-.72%204.652%201.36%204.15%203.788l-11.698%2056.621c-.732%203.542%203.979%205.473%205.943%202.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505%204.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CLI Proxy API Management Center</title>
|
||||
|
||||
@@ -408,10 +408,9 @@
|
||||
|
||||
.floatingSidebarContainer {
|
||||
position: fixed;
|
||||
left: var(--visual-config-floating-left, 16px);
|
||||
top: var(--visual-config-floating-top, 120px);
|
||||
width: var(--visual-config-floating-width, 280px);
|
||||
max-height: var(--visual-config-floating-max-height, calc(100vh - 136px));
|
||||
left: 0;
|
||||
top: 0;
|
||||
will-change: transform, width, max-height;
|
||||
z-index: 45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@@ -429,9 +428,7 @@
|
||||
padding: 12px;
|
||||
border-radius: 26px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-primary) 76%, transparent);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
background: color-mix(in srgb, var(--bg-primary) 96%, transparent);
|
||||
box-shadow: 0 24px 56px -34px rgba(0, 0, 0, 0.42);
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -392,10 +392,9 @@ export function VisualConfigEditor({
|
||||
if (!floatingElement) return undefined;
|
||||
|
||||
const clearFloatingStyles = () => {
|
||||
floatingElement.style.removeProperty('--visual-config-floating-left');
|
||||
floatingElement.style.removeProperty('--visual-config-floating-top');
|
||||
floatingElement.style.removeProperty('--visual-config-floating-width');
|
||||
floatingElement.style.removeProperty('--visual-config-floating-max-height');
|
||||
floatingElement.style.removeProperty('transform');
|
||||
floatingElement.style.removeProperty('width');
|
||||
floatingElement.style.removeProperty('max-height');
|
||||
floatingElement.style.removeProperty('opacity');
|
||||
floatingElement.style.removeProperty('pointer-events');
|
||||
};
|
||||
@@ -405,7 +404,8 @@ export function VisualConfigEditor({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const getHeaderHeight = () => {
|
||||
/* ---- Cache header height – recomputed only on resize ---- */
|
||||
const computeHeaderHeight = () => {
|
||||
const header = document.querySelector('.main-header') as HTMLElement | null;
|
||||
if (header) return header.getBoundingClientRect().height;
|
||||
|
||||
@@ -413,8 +413,14 @@ export function VisualConfigEditor({
|
||||
const parsed = Number.parseFloat(raw);
|
||||
return Number.isFinite(parsed) ? parsed : 64;
|
||||
};
|
||||
let headerHeight = computeHeaderHeight();
|
||||
|
||||
/* ---- Cache content scroller – resolved once ---- */
|
||||
const contentScroller = document.querySelector('.content') as HTMLElement | null;
|
||||
|
||||
/* ---- Cache floating height from previous frame ---- */
|
||||
let cachedFloatingHeight = floatingElement.getBoundingClientRect().height || 200;
|
||||
|
||||
const getContentScroller = () => document.querySelector('.content') as HTMLElement | null;
|
||||
let frameId = 0;
|
||||
|
||||
const updateFloatingPosition = () => {
|
||||
@@ -422,10 +428,9 @@ export function VisualConfigEditor({
|
||||
|
||||
const anchorRect = anchorElement.getBoundingClientRect();
|
||||
const workspaceRect = workspaceElement.getBoundingClientRect();
|
||||
const floatingHeight = floatingElement.getBoundingClientRect().height;
|
||||
const stickyTop = getHeaderHeight() + 20;
|
||||
const stickyTop = headerHeight + 20;
|
||||
const viewportPadding = 16;
|
||||
const maxTop = workspaceRect.bottom - floatingHeight;
|
||||
const maxTop = workspaceRect.bottom - cachedFloatingHeight;
|
||||
const unclampedTop = Math.min(Math.max(anchorRect.top, stickyTop), maxTop);
|
||||
const top = Math.max(unclampedTop, viewportPadding);
|
||||
const left = Math.max(anchorRect.left, viewportPadding);
|
||||
@@ -436,10 +441,9 @@ export function VisualConfigEditor({
|
||||
const maxHeight = Math.max(window.innerHeight - top - viewportPadding, 160);
|
||||
const isVisible = workspaceRect.bottom > stickyTop + 24 && anchorRect.top < window.innerHeight;
|
||||
|
||||
floatingElement.style.setProperty('--visual-config-floating-left', `${left}px`);
|
||||
floatingElement.style.setProperty('--visual-config-floating-top', `${top}px`);
|
||||
floatingElement.style.setProperty('--visual-config-floating-width', `${width}px`);
|
||||
floatingElement.style.setProperty('--visual-config-floating-max-height', `${maxHeight}px`);
|
||||
floatingElement.style.transform = `translate3d(${left}px, ${top}px, 0)`;
|
||||
floatingElement.style.width = `${width}px`;
|
||||
floatingElement.style.maxHeight = `${maxHeight}px`;
|
||||
floatingElement.style.opacity = isVisible ? '1' : '0';
|
||||
floatingElement.style.pointerEvents = isVisible ? 'auto' : 'none';
|
||||
};
|
||||
@@ -449,10 +453,15 @@ export function VisualConfigEditor({
|
||||
frameId = requestAnimationFrame(updateFloatingPosition);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
headerHeight = computeHeaderHeight();
|
||||
cachedFloatingHeight = floatingElement.getBoundingClientRect().height || cachedFloatingHeight;
|
||||
requestPositionUpdate();
|
||||
};
|
||||
|
||||
requestPositionUpdate();
|
||||
|
||||
const contentScroller = getContentScroller();
|
||||
window.addEventListener('resize', requestPositionUpdate);
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('scroll', requestPositionUpdate, { passive: true });
|
||||
contentScroller?.addEventListener('scroll', requestPositionUpdate, { passive: true });
|
||||
|
||||
@@ -460,12 +469,11 @@ export function VisualConfigEditor({
|
||||
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(requestPositionUpdate);
|
||||
resizeObserver?.observe(anchorElement);
|
||||
resizeObserver?.observe(workspaceElement);
|
||||
resizeObserver?.observe(floatingElement);
|
||||
|
||||
return () => {
|
||||
if (frameId) cancelAnimationFrame(frameId);
|
||||
resizeObserver?.disconnect();
|
||||
window.removeEventListener('resize', requestPositionUpdate);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('scroll', requestPositionUpdate);
|
||||
contentScroller?.removeEventListener('scroll', requestPositionUpdate);
|
||||
clearFloatingStyles();
|
||||
|
||||
@@ -83,7 +83,11 @@ export function ServiceHealthCard({ usage, loading }: ServiceHealthCardProps) {
|
||||
}, [activeTooltip]);
|
||||
|
||||
const buildTooltipState = useCallback(
|
||||
(idx: number, anchorEl: HTMLDivElement): ActiveTooltipState => {
|
||||
(idx: number, anchorEl: HTMLDivElement | null): ActiveTooltipState | null => {
|
||||
if (!anchorEl || !anchorEl.isConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
|
||||
@@ -161,9 +165,8 @@ export function ServiceHealthCard({ usage, loading }: ServiceHealthCardProps) {
|
||||
(e: React.PointerEvent<HTMLDivElement>, idx: number) => {
|
||||
if (e.pointerType === 'touch') {
|
||||
e.preventDefault();
|
||||
setActiveTooltip((prev) =>
|
||||
prev?.idx === idx ? null : buildTooltipState(idx, e.currentTarget)
|
||||
);
|
||||
const anchorEl = e.currentTarget;
|
||||
setActiveTooltip((prev) => (prev?.idx === idx ? null : buildTooltipState(idx, anchorEl)));
|
||||
}
|
||||
},
|
||||
[buildTooltipState]
|
||||
|
||||
@@ -21,13 +21,23 @@ const AUTH_FILES_SORT_MODE_SET = new Set<AuthFilesSortMode>(AUTH_FILES_SORT_MODE
|
||||
export const isAuthFilesSortMode = (value: unknown): value is AuthFilesSortMode =>
|
||||
typeof value === 'string' && AUTH_FILES_SORT_MODE_SET.has(value as AuthFilesSortMode);
|
||||
|
||||
const readAuthFilesUiStateFromStorage = (
|
||||
storage: Pick<Storage, 'getItem'> | null | undefined
|
||||
): AuthFilesUiState | null => {
|
||||
if (!storage) return null;
|
||||
const raw = storage.getItem(AUTH_FILES_UI_STATE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as AuthFilesUiState;
|
||||
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||
};
|
||||
|
||||
export const readAuthFilesUiState = (): AuthFilesUiState | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(AUTH_FILES_UI_STATE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as AuthFilesUiState;
|
||||
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||
return (
|
||||
readAuthFilesUiStateFromStorage(window.localStorage) ??
|
||||
readAuthFilesUiStateFromStorage(window.sessionStorage)
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -36,7 +46,12 @@ export const readAuthFilesUiState = (): AuthFilesUiState | null => {
|
||||
export const writeAuthFilesUiState = (state: AuthFilesUiState) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
window.sessionStorage.setItem(AUTH_FILES_UI_STATE_KEY, JSON.stringify(state));
|
||||
window.localStorage.setItem(AUTH_FILES_UI_STATE_KEY, JSON.stringify(state));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
window.sessionStorage.removeItem(AUTH_FILES_UI_STATE_KEY);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -140,7 +140,17 @@
|
||||
"edit_settings": "Edit Settings",
|
||||
"routing_strategy": "Routing Strategy",
|
||||
"available_models": "Available Models",
|
||||
"available_models_desc": "Total models from all providers"
|
||||
"available_models_desc": "Total models from all providers",
|
||||
"welcome_back": "Welcome Back",
|
||||
"greeting_morning": "Good Morning",
|
||||
"greeting_afternoon": "Good Afternoon",
|
||||
"greeting_evening": "Good Evening",
|
||||
"greeting_night": "Good Night",
|
||||
"caring_morning": "A fresh start — let's make today count.",
|
||||
"caring_afternoon": "Steady progress — you're doing great.",
|
||||
"caring_evening": "Wrapping up nicely — almost there.",
|
||||
"caring_night": "Burning the midnight oil? Don't forget to rest.",
|
||||
"system_overview": "System Overview"
|
||||
},
|
||||
"basic_settings": {
|
||||
"title": "Basic Settings",
|
||||
|
||||
@@ -140,7 +140,17 @@
|
||||
"edit_settings": "Изменить настройки",
|
||||
"routing_strategy": "Стратегия маршрутизации",
|
||||
"available_models": "Доступные модели",
|
||||
"available_models_desc": "Всего моделей от всех провайдеров"
|
||||
"available_models_desc": "Всего моделей от всех провайдеров",
|
||||
"welcome_back": "С возвращением",
|
||||
"greeting_morning": "Доброе утро",
|
||||
"greeting_afternoon": "Добрый день",
|
||||
"greeting_evening": "Добрый вечер",
|
||||
"greeting_night": "Доброй ночи",
|
||||
"caring_morning": "Новый день — начнём продуктивно.",
|
||||
"caring_afternoon": "Уверенный прогресс — отличная работа.",
|
||||
"caring_evening": "День подходит к концу — финальный рывок.",
|
||||
"caring_night": "Поздняя работа? Не забудьте отдохнуть.",
|
||||
"system_overview": "Обзор системы"
|
||||
},
|
||||
"basic_settings": {
|
||||
"title": "Основные настройки",
|
||||
|
||||
@@ -140,7 +140,17 @@
|
||||
"edit_settings": "编辑设置",
|
||||
"routing_strategy": "路由策略",
|
||||
"available_models": "可用模型",
|
||||
"available_models_desc": "所有提供商的模型总数"
|
||||
"available_models_desc": "所有提供商的模型总数",
|
||||
"welcome_back": "欢迎回来",
|
||||
"greeting_morning": "早上好",
|
||||
"greeting_afternoon": "下午好",
|
||||
"greeting_evening": "晚上好",
|
||||
"greeting_night": "夜深了",
|
||||
"caring_morning": "新的一天,准备大展身手吧。",
|
||||
"caring_afternoon": "稳步推进中,继续加油。",
|
||||
"caring_evening": "今天辛苦了,收尾工作做好哦。",
|
||||
"caring_night": "夜深了,别忘了早些休息。",
|
||||
"system_overview": "系统概览"
|
||||
},
|
||||
"basic_settings": {
|
||||
"title": "基础设置",
|
||||
|
||||
@@ -5,6 +5,8 @@ import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
|
||||
import App from './App.tsx';
|
||||
|
||||
document.title = 'CLI Proxy API Management Center';
|
||||
document.documentElement.setAttribute('translate', 'no');
|
||||
document.documentElement.classList.add('notranslate');
|
||||
|
||||
const faviconEl = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
||||
if (faviconEl) {
|
||||
|
||||
+364
-173
@@ -1,59 +1,234 @@
|
||||
@use 'sass:color';
|
||||
@use '../styles/variables.scss' as *;
|
||||
|
||||
// ─── Container ──────────────────────────────────────────
|
||||
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-lg;
|
||||
gap: $spacing-xl;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: $spacing-sm;
|
||||
// ─── Decorative Background Orbs ─────────────────────────
|
||||
|
||||
.backgroundOrbs {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
.orb1 {
|
||||
position: absolute;
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
color-mix(in srgb, var(--primary-color) 8%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
top: -140px;
|
||||
right: -80px;
|
||||
animation: orbFloat 22s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.orb2 {
|
||||
position: absolute;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
color-mix(in srgb, var(--success-color) 6%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
bottom: 18%;
|
||||
left: -100px;
|
||||
animation: orbFloat 28s ease-in-out infinite alternate-reverse;
|
||||
}
|
||||
|
||||
@keyframes orbFloat {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: translate(30px, -20px) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Hero Welcome Section ───────────────────────────────
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-2xl $spacing-xl;
|
||||
border-radius: $radius-lg;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--bg-primary) 92%, transparent),
|
||||
color-mix(in srgb, var(--bg-secondary) 80%, transparent)
|
||||
);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
overflow: hidden;
|
||||
animation: heroEnter 0.6s ease-out both;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: $spacing-xl $spacing-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Large watermark text behind hero content
|
||||
.heroWatermark {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: $spacing-xl;
|
||||
transform: translateY(-50%);
|
||||
font-size: clamp(64px, 12vw, 120px);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
opacity: 0.04;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
animation: watermarkEnter 0.8s ease-out 0.1s both;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
font-size: clamp(48px, 14vw, 80px);
|
||||
left: $spacing-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@keyframes watermarkEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 0.04;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.heroContent {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
|
||||
.heroGreeting {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--primary-color);
|
||||
animation: fadeSlideUp 0.5s ease-out 0.1s both;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
margin: 0;
|
||||
font-size: clamp(32px, 5vw, 48px);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--text-primary);
|
||||
animation: fadeSlideUp 0.5s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.heroCaring {
|
||||
margin: $spacing-xs 0 0;
|
||||
font-size: 15px;
|
||||
color: var(--text-secondary);
|
||||
margin: $spacing-xs 0 0 0;
|
||||
line-height: 1.5;
|
||||
animation: fadeSlideUp 0.5s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
.connectionCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-md;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
// ─── Hero Meta (right side) ─────────────────────────────
|
||||
|
||||
.connectionStatus {
|
||||
.heroMeta {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: $spacing-sm;
|
||||
flex-shrink: 0;
|
||||
animation: fadeSlideUp 0.5s ease-out 0.35s both;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.dateTimeBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
// ─── Connection Pill ────────────────────────────────────
|
||||
|
||||
.connectionPill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 12px;
|
||||
border-radius: $radius-full;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 82%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: $gray-400;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.connected {
|
||||
background: $success-color;
|
||||
box-shadow: 0 0 8px rgba($success-color, 0.5);
|
||||
box-shadow: 0 0 6px rgba($success-color, 0.5);
|
||||
}
|
||||
|
||||
&.connecting {
|
||||
@@ -66,54 +241,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.statusText {
|
||||
.pillText {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.connectionInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.serverUrl {
|
||||
font-family: $font-mono;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-primary);
|
||||
padding: 4px 10px;
|
||||
border-radius: $radius-md;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.serverVersion {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
background: rgba($primary-color, 0.1);
|
||||
padding: 4px 10px;
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
.buildDate {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: right;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
// ─── Bento Stats Grid ───────────────────────────────────
|
||||
|
||||
.statsSection {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sectionHeading {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 0 $spacing-md;
|
||||
}
|
||||
|
||||
.bentoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
gap: $spacing-md;
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -125,150 +287,175 @@
|
||||
}
|
||||
}
|
||||
|
||||
.statCard {
|
||||
.bentoCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-lg;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-lg;
|
||||
text-decoration: none;
|
||||
transition: all $transition-fast;
|
||||
transition:
|
||||
border-color $transition-fast,
|
||||
box-shadow $transition-fast,
|
||||
transform $transition-fast;
|
||||
animation: cardEnter 0.4s ease-out both;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
// First card spans 2 rows — the "hero stat"
|
||||
.bentoLarge {
|
||||
grid-row: 1 / 3;
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
160deg,
|
||||
color-mix(in srgb, var(--primary-color) 6%, var(--bg-primary)),
|
||||
var(--bg-primary)
|
||||
);
|
||||
|
||||
.bentoValue {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.bentoIcon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
grid-row: auto;
|
||||
|
||||
.bentoValue {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.bentoIcon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bentoIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: $radius-md;
|
||||
background: var(--bg-secondary);
|
||||
background: color-mix(in srgb, var(--primary-color) 10%, var(--bg-secondary));
|
||||
color: var(--primary-color);
|
||||
transition: background $transition-fast;
|
||||
}
|
||||
|
||||
.statContent {
|
||||
.bentoCard:hover .bentoIcon {
|
||||
background: color-mix(in srgb, var(--primary-color) 16%, var(--bg-secondary));
|
||||
}
|
||||
|
||||
.bentoContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 24px;
|
||||
.bentoValue {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
.bentoLabel {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.statSublabel {
|
||||
.bentoSublabel {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.8;
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.section {
|
||||
// ─── Config Pills Section ───────────────────────────────
|
||||
|
||||
.configSection {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
animation: cardEnter 0.4s ease-out 0.5s both;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionsGrid {
|
||||
.configPillGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
.configPill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
|
||||
// Button 内部的 span 需要 flex 对齐图标和文字
|
||||
> span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.configGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.configItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-sm;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
padding: 6px 14px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-full;
|
||||
font-size: 13px;
|
||||
transition: border-color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.configPillWide {
|
||||
flex-basis: 100%;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
|
||||
.configLabel {
|
||||
font-size: 13px;
|
||||
.configPillLabel {
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.configValue {
|
||||
font-size: 13px;
|
||||
.configPillValue {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
|
||||
&.enabled {
|
||||
&.on {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
&.off {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.configValueMono {
|
||||
.configPillMono {
|
||||
font-size: 12px;
|
||||
font-family: $font-mono;
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// Config badges (routing strategy)
|
||||
.configBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: $radius-full;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
@@ -294,50 +481,7 @@
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.configItemFull {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
// Usage stats section
|
||||
.usageGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.usageCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-md;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.usageValue {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.usageLabel {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.usageLoading,
|
||||
.usageEmpty {
|
||||
padding: $spacing-lg;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
// ─── View More Link ─────────────────────────────────────
|
||||
|
||||
.viewMoreLink {
|
||||
display: inline-flex;
|
||||
@@ -346,8 +490,55 @@
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
margin-top: $spacing-xs;
|
||||
transition: color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Animations ─────────────────────────────────────────
|
||||
|
||||
@keyframes heroEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cardEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
+129
-68
@@ -27,6 +27,16 @@ interface ProviderStats {
|
||||
openai: number | null;
|
||||
}
|
||||
|
||||
type TimeOfDay = 'morning' | 'afternoon' | 'evening' | 'night';
|
||||
|
||||
function getTimeOfDay(): TimeOfDay {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 5 && hour < 12) return 'morning';
|
||||
if (hour >= 12 && hour < 17) return 'afternoon';
|
||||
if (hour >= 17 && hour < 21) return 'evening';
|
||||
return 'night';
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||
@@ -56,12 +66,25 @@ export function DashboardPage() {
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Time-of-day state for dynamic greeting
|
||||
const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>(getTimeOfDay);
|
||||
const [currentTime, setCurrentTime] = useState(() => new Date());
|
||||
|
||||
const apiKeysCache = useRef<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
apiKeysCache.current = [];
|
||||
}, [apiBase, config?.apiKeys]);
|
||||
|
||||
// Update time every 60 seconds
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setTimeOfDay(getTimeOfDay());
|
||||
setCurrentTime(new Date());
|
||||
}, 60_000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const normalizeApiKeyList = (input: unknown): string[] => {
|
||||
if (!Array.isArray(input)) return [];
|
||||
const seen = new Set<string>();
|
||||
@@ -237,113 +260,151 @@ export function DashboardPage() {
|
||||
? styles.configBadgeFillFirst
|
||||
: styles.configBadgeUnknown;
|
||||
|
||||
// Derived time-based values
|
||||
const greetingKey = `dashboard.greeting_${timeOfDay}`;
|
||||
const caringKey = `dashboard.caring_${timeOfDay}`;
|
||||
|
||||
const formattedDate = currentTime.toLocaleDateString(i18n.language, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
const formattedTime = currentTime.toLocaleTimeString(i18n.language, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.dashboard}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>{t('dashboard.title')}</h1>
|
||||
<p className={styles.subtitle}>{t('dashboard.subtitle')}</p>
|
||||
{/* Decorative background orbs */}
|
||||
<div className={styles.backgroundOrbs} aria-hidden="true">
|
||||
<div className={styles.orb1} />
|
||||
<div className={styles.orb2} />
|
||||
</div>
|
||||
|
||||
<div className={styles.connectionCard}>
|
||||
<div className={styles.connectionStatus}>
|
||||
<span
|
||||
className={`${styles.statusDot} ${
|
||||
connectionStatus === 'connected'
|
||||
? styles.connected
|
||||
: connectionStatus === 'connecting'
|
||||
? styles.connecting
|
||||
: styles.disconnected
|
||||
}`}
|
||||
/>
|
||||
<span className={styles.statusText}>
|
||||
{t(
|
||||
connectionStatus === 'connected'
|
||||
? 'common.connected'
|
||||
: connectionStatus === 'connecting'
|
||||
? 'common.connecting'
|
||||
: 'common.disconnected'
|
||||
)}
|
||||
</span>
|
||||
{/* Hero welcome section */}
|
||||
<section className={styles.hero}>
|
||||
<span className={styles.heroWatermark} aria-hidden="true">
|
||||
OVERVIEW
|
||||
</span>
|
||||
<div className={styles.heroContent}>
|
||||
<span className={styles.heroGreeting}>{t(greetingKey)}</span>
|
||||
<h1 className={styles.heroTitle}>{t('dashboard.welcome_back')}</h1>
|
||||
<p className={styles.heroCaring}>{t(caringKey)}</p>
|
||||
</div>
|
||||
<div className={styles.connectionInfo}>
|
||||
<span className={styles.serverUrl}>{apiBase || '-'}</span>
|
||||
{serverVersion && (
|
||||
<span className={styles.serverVersion}>
|
||||
v{serverVersion.trim().replace(/^[vV]+/, '')}
|
||||
<div className={styles.heroMeta}>
|
||||
<div className={styles.dateTimeBlock}>
|
||||
<span className={styles.time}>{formattedTime}</span>
|
||||
<span className={styles.date}>{formattedDate}</span>
|
||||
</div>
|
||||
<div className={styles.connectionPill}>
|
||||
<span
|
||||
className={`${styles.statusDot} ${
|
||||
connectionStatus === 'connected'
|
||||
? styles.connected
|
||||
: connectionStatus === 'connecting'
|
||||
? styles.connecting
|
||||
: styles.disconnected
|
||||
}`}
|
||||
/>
|
||||
<span className={styles.pillText}>
|
||||
{serverVersion
|
||||
? `v${serverVersion.trim().replace(/^[vV]+/, '')}`
|
||||
: t(
|
||||
connectionStatus === 'connected'
|
||||
? 'common.connected'
|
||||
: connectionStatus === 'connecting'
|
||||
? 'common.connecting'
|
||||
: 'common.disconnected'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{serverBuildDate && (
|
||||
<span className={styles.buildDate}>
|
||||
{new Date(serverBuildDate).toLocaleDateString(i18n.language)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className={styles.statsGrid}>
|
||||
{quickStats.map((stat) => (
|
||||
<Link key={stat.path} to={stat.path} className={styles.statCard}>
|
||||
<div className={styles.statIcon}>{stat.icon}</div>
|
||||
<div className={styles.statContent}>
|
||||
<span className={styles.statValue}>{stat.loading ? '...' : stat.value}</span>
|
||||
<span className={styles.statLabel}>{stat.label}</span>
|
||||
{stat.sublabel && !stat.loading && (
|
||||
<span className={styles.statSublabel}>{stat.sublabel}</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{/* Bento stats grid */}
|
||||
<section className={styles.statsSection}>
|
||||
<h2 className={styles.sectionHeading}>{t('dashboard.system_overview')}</h2>
|
||||
<div className={styles.bentoGrid}>
|
||||
{quickStats.map((stat, index) => (
|
||||
<Link
|
||||
key={stat.path}
|
||||
to={stat.path}
|
||||
className={`${styles.bentoCard} ${index === 0 ? styles.bentoLarge : ''}`}
|
||||
style={{ animationDelay: `${index * 80}ms` }}
|
||||
>
|
||||
<div className={styles.bentoIcon}>{stat.icon}</div>
|
||||
<div className={styles.bentoContent}>
|
||||
<span className={styles.bentoValue}>
|
||||
{stat.loading ? '...' : stat.value}
|
||||
</span>
|
||||
<span className={styles.bentoLabel}>{stat.label}</span>
|
||||
{stat.sublabel && !stat.loading && (
|
||||
<span className={styles.bentoSublabel}>{stat.sublabel}</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Config pills section */}
|
||||
{config && (
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>{t('dashboard.current_config')}</h2>
|
||||
<div className={styles.configGrid}>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>{t('basic_settings.debug_enable')}</span>
|
||||
<span className={`${styles.configValue} ${config.debug ? styles.enabled : styles.disabled}`}>
|
||||
<section className={styles.configSection}>
|
||||
<h2 className={styles.sectionHeading}>{t('dashboard.current_config')}</h2>
|
||||
<div className={styles.configPillGrid}>
|
||||
<div className={styles.configPill}>
|
||||
<span className={styles.configPillLabel}>{t('basic_settings.debug_enable')}</span>
|
||||
<span className={`${styles.configPillValue} ${config.debug ? styles.on : styles.off}`}>
|
||||
{config.debug ? t('common.yes') : t('common.no')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>{t('basic_settings.usage_statistics_enable')}</span>
|
||||
<span className={`${styles.configValue} ${config.usageStatisticsEnabled ? styles.enabled : styles.disabled}`}>
|
||||
<div className={styles.configPill}>
|
||||
<span className={styles.configPillLabel}>{t('basic_settings.usage_statistics_enable')}</span>
|
||||
<span className={`${styles.configPillValue} ${config.usageStatisticsEnabled ? styles.on : styles.off}`}>
|
||||
{config.usageStatisticsEnabled ? t('common.yes') : t('common.no')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>{t('basic_settings.logging_to_file_enable')}</span>
|
||||
<span className={`${styles.configValue} ${config.loggingToFile ? styles.enabled : styles.disabled}`}>
|
||||
<div className={styles.configPill}>
|
||||
<span className={styles.configPillLabel}>{t('basic_settings.logging_to_file_enable')}</span>
|
||||
<span className={`${styles.configPillValue} ${config.loggingToFile ? styles.on : styles.off}`}>
|
||||
{config.loggingToFile ? t('common.yes') : t('common.no')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>{t('basic_settings.retry_count_label')}</span>
|
||||
<span className={styles.configValue}>{config.requestRetry ?? 0}</span>
|
||||
<div className={styles.configPill}>
|
||||
<span className={styles.configPillLabel}>{t('basic_settings.retry_count_label')}</span>
|
||||
<span className={styles.configPillValue}>{config.requestRetry ?? 0}</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>{t('basic_settings.ws_auth_enable')}</span>
|
||||
<span className={`${styles.configValue} ${config.wsAuth ? styles.enabled : styles.disabled}`}>
|
||||
<div className={styles.configPill}>
|
||||
<span className={styles.configPillLabel}>{t('basic_settings.ws_auth_enable')}</span>
|
||||
<span className={`${styles.configPillValue} ${config.wsAuth ? styles.on : styles.off}`}>
|
||||
{config.wsAuth ? t('common.yes') : t('common.no')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>{t('dashboard.routing_strategy')}</span>
|
||||
<div className={styles.configPill}>
|
||||
<span className={styles.configPillLabel}>{t('dashboard.routing_strategy')}</span>
|
||||
<span className={`${styles.configBadge} ${routingStrategyBadgeClass}`}>
|
||||
{routingStrategyDisplay}
|
||||
</span>
|
||||
</div>
|
||||
{config.proxyUrl && (
|
||||
<div className={`${styles.configItem} ${styles.configItemFull}`}>
|
||||
<span className={styles.configLabel}>{t('basic_settings.proxy_url_label')}</span>
|
||||
<span className={styles.configValueMono}>{config.proxyUrl}</span>
|
||||
<div className={`${styles.configPill} ${styles.configPillWide}`}>
|
||||
<span className={styles.configPillLabel}>{t('basic_settings.proxy_url_label')}</span>
|
||||
<span className={styles.configPillMono}>{config.proxyUrl}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Link to="/config" className={styles.viewMoreLink}>
|
||||
{t('dashboard.edit_settings')} →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user