mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat:update icon
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useNotificationStore } from '@/stores';
|
import { useNotificationStore } from '@/stores';
|
||||||
|
import { IconX } from '@/components/ui/icons';
|
||||||
import type { Notification } from '@/types';
|
import type { Notification } from '@/types';
|
||||||
|
|
||||||
interface AnimatedNotification extends Notification {
|
interface AnimatedNotification extends Notification {
|
||||||
@@ -83,8 +84,8 @@ export function NotificationContainer() {
|
|||||||
className={`notification ${notification.type} ${notification.isExiting ? 'exiting' : 'entering'}`}
|
className={`notification ${notification.type} ${notification.isExiting ? 'exiting' : 'entering'}`}
|
||||||
>
|
>
|
||||||
<div className="message">{notification.message}</div>
|
<div className="message">{notification.message}</div>
|
||||||
<button className="close-btn" onClick={() => handleClose(notification.id)}>
|
<button className="close-btn" onClick={() => handleClose(notification.id)} aria-label="Close">
|
||||||
×
|
<IconX size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,97 +2,31 @@ import { ReactNode, SVGProps, useCallback, useEffect, useLayoutEffect, useRef, u
|
|||||||
import { NavLink, Outlet } from 'react-router-dom';
|
import { NavLink, Outlet } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import {
|
||||||
|
IconBot,
|
||||||
|
IconChartLine,
|
||||||
|
IconFileText,
|
||||||
|
IconInfo,
|
||||||
|
IconKey,
|
||||||
|
IconScrollText,
|
||||||
|
IconSettings,
|
||||||
|
IconShield,
|
||||||
|
IconSlidersHorizontal
|
||||||
|
} from '@/components/ui/icons';
|
||||||
import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
|
import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
|
||||||
import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||||
import { versionApi } from '@/services/api';
|
import { versionApi } from '@/services/api';
|
||||||
|
|
||||||
const iconProps: SVGProps<SVGSVGElement> = {
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
viewBox: '0 0 20 20',
|
|
||||||
fill: 'none',
|
|
||||||
stroke: 'currentColor',
|
|
||||||
strokeWidth: 1.5,
|
|
||||||
strokeLinecap: 'round',
|
|
||||||
strokeLinejoin: 'round',
|
|
||||||
'aria-hidden': 'true',
|
|
||||||
focusable: 'false'
|
|
||||||
};
|
|
||||||
|
|
||||||
const sidebarIcons: Record<string, ReactNode> = {
|
const sidebarIcons: Record<string, ReactNode> = {
|
||||||
settings: (
|
settings: <IconSlidersHorizontal size={18} />,
|
||||||
<svg {...iconProps}>
|
apiKeys: <IconKey size={18} />,
|
||||||
<path d="M4 6.5h12" />
|
aiProviders: <IconBot size={18} />,
|
||||||
<circle cx="9" cy="6.5" r="2" />
|
authFiles: <IconFileText size={18} />,
|
||||||
<path d="M4 10h12" />
|
oauth: <IconShield size={18} />,
|
||||||
<circle cx="7" cy="10" r="2" />
|
usage: <IconChartLine size={18} />,
|
||||||
<path d="M4 13.5h12" />
|
config: <IconSettings size={18} />,
|
||||||
<circle cx="12" cy="13.5" r="2" />
|
logs: <IconScrollText size={18} />,
|
||||||
</svg>
|
system: <IconInfo size={18} />
|
||||||
),
|
|
||||||
apiKeys: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<circle cx="7.2" cy="10" r="2.4" />
|
|
||||||
<path d="M9.6 10h6" />
|
|
||||||
<path d="M12.8 10v2.4" />
|
|
||||||
<path d="M14.8 10v1.4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
aiProviders: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<circle cx="10" cy="5.2" r="2.2" />
|
|
||||||
<circle cx="6" cy="13.2" r="2" />
|
|
||||||
<circle cx="14" cy="13.2" r="2" />
|
|
||||||
<path d="M8.6 6.8 6.8 10.8" />
|
|
||||||
<path d="M11.4 6.8 13.2 10.8" />
|
|
||||||
<path d="M7.8 13.2h4.4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
authFiles: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<path d="M7 3.5h4.8L15 6.8V16H7Z" />
|
|
||||||
<path d="M11.8 3.5V7h3.2" />
|
|
||||||
<path d="m8.9 11.8 1.7 1.6 3.4-3.5" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
oauth: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<path d="M10 3.5 15.2 5.6v3.6c0 3-2 5.8-5.2 7-3.2-1.2-5.2-4-5.2-7V5.6Z" />
|
|
||||||
<path d="M8.2 9.6h3.6" />
|
|
||||||
<path d="m9.6 8.2-1.4 1.4 1.4 1.4" />
|
|
||||||
<path d="m11.8 8.2 1.4 1.4-1.4 1.4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
usage: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<path d="M4 14.5h12" />
|
|
||||||
<path d="m6.2 11.3 3-3 2.4 2 2.9-3.7" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
config: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<path d="M5.2 8 10 5.8l4.8 2.2L10 10.2Z" />
|
|
||||||
<path d="M5.2 12 10 9.8l4.8 2.2L10 14.2Z" />
|
|
||||||
<path d="M10 10.2v3.6" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
logs: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<path d="M6.4 6h9" />
|
|
||||||
<path d="M6.4 10h9" />
|
|
||||||
<path d="M6.4 14h9" />
|
|
||||||
<circle cx="4.2" cy="6" r="0.9" />
|
|
||||||
<circle cx="4.2" cy="10" r="0.9" />
|
|
||||||
<circle cx="4.2" cy="14" r="0.9" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
system: (
|
|
||||||
<svg {...iconProps}>
|
|
||||||
<circle cx="10" cy="10" r="6.2" />
|
|
||||||
<path d="M10 8.8v3.6" />
|
|
||||||
<circle cx="10" cy="6.2" r="0.8" fill="currentColor" stroke="none" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Header action icons - smaller size for header buttons
|
// Header action icons - smaller size for header buttons
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { IconInbox } from './icons';
|
||||||
|
|
||||||
interface EmptyStateProps {
|
interface EmptyStateProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -11,7 +12,7 @@ export function EmptyState({ title, description, action }: EmptyStateProps) {
|
|||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<div className="empty-content">
|
<div className="empty-content">
|
||||||
<div className="empty-icon" aria-hidden="true">
|
<div className="empty-icon" aria-hidden="true">
|
||||||
◦
|
<IconInbox size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="empty-title">{title}</div>
|
<div className="empty-title">{title}</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import { IconX } from './icons';
|
||||||
import type { HeaderEntry } from '@/utils/headers';
|
import type { HeaderEntry } from '@/utils/headers';
|
||||||
|
|
||||||
interface HeaderInputListProps {
|
interface HeaderInputListProps {
|
||||||
@@ -60,8 +61,10 @@ export function HeaderInputList({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeEntry(index)}
|
onClick={() => removeEntry(index)}
|
||||||
disabled={disabled || currentEntries.length <= 1}
|
disabled={disabled || currentEntries.length <= 1}
|
||||||
|
title="Remove"
|
||||||
|
aria-label="Remove"
|
||||||
>
|
>
|
||||||
✕
|
<IconX size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
import { IconX } from './icons';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -23,7 +24,7 @@ export function Modal({ open, title, onClose, footer, width = 520, children }: P
|
|||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<div className="modal-title">{title}</div>
|
<div className="modal-title">{title}</div>
|
||||||
<button className="modal-close" onClick={onClose} aria-label="Close">
|
<button className="modal-close" onClick={onClose} aria-label="Close">
|
||||||
×
|
<IconX size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">{children}</div>
|
<div className="modal-body">{children}</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import { IconX } from './icons';
|
||||||
import type { ModelAlias } from '@/types';
|
import type { ModelAlias } from '@/types';
|
||||||
|
|
||||||
interface ModelEntry {
|
interface ModelEntry {
|
||||||
@@ -88,8 +89,10 @@ export function ModelInputList({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeEntry(index)}
|
onClick={() => removeEntry(index)}
|
||||||
disabled={disabled || currentEntries.length <= 1}
|
disabled={disabled || currentEntries.length <= 1}
|
||||||
|
title="Remove"
|
||||||
|
aria-label="Remove"
|
||||||
>
|
>
|
||||||
✕
|
<IconX size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
260
src/components/ui/icons.tsx
Normal file
260
src/components/ui/icons.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import type { SVGProps } from 'react';
|
||||||
|
|
||||||
|
// Inline SVG icons (Lucide, ISC). We embed paths to keep the WebUI single-file/offline friendly.
|
||||||
|
// Source: https://github.com/lucide-icons/lucide (via lucide-static).
|
||||||
|
|
||||||
|
export interface IconProps extends SVGProps<SVGSVGElement> {
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseSvgProps: SVGProps<SVGSVGElement> = {
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
'aria-hidden': 'true',
|
||||||
|
focusable: 'false'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IconSlidersHorizontal({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<line x1="21" x2="14" y1="4" y2="4" />
|
||||||
|
<line x1="10" x2="3" y1="4" y2="4" />
|
||||||
|
<line x1="21" x2="12" y1="12" y2="12" />
|
||||||
|
<line x1="8" x2="3" y1="12" y2="12" />
|
||||||
|
<line x1="21" x2="16" y1="20" y2="20" />
|
||||||
|
<line x1="12" x2="3" y1="20" y2="20" />
|
||||||
|
<line x1="14" x2="14" y1="2" y2="6" />
|
||||||
|
<line x1="8" x2="8" y1="10" y2="14" />
|
||||||
|
<line x1="16" x2="16" y1="18" y2="22" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconKey({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4" />
|
||||||
|
<path d="m21 2-9.6 9.6" />
|
||||||
|
<circle cx="7.5" cy="15.5" r="5.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconBot({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M12 8V4H8" />
|
||||||
|
<rect width="16" height="12" x="4" y="8" rx="2" />
|
||||||
|
<path d="M2 14h2" />
|
||||||
|
<path d="M20 14h2" />
|
||||||
|
<path d="M15 13v2" />
|
||||||
|
<path d="M9 13v2" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconFileText({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||||
|
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||||
|
<path d="M10 9H8" />
|
||||||
|
<path d="M16 13H8" />
|
||||||
|
<path d="M16 17H8" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconShield({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconChartLine({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M3 3v16a2 2 0 0 0 2 2h16" />
|
||||||
|
<path d="m19 9-5 5-4-4-3 3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconSettings({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconScrollText({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M15 12h-5" />
|
||||||
|
<path d="M15 8h-5" />
|
||||||
|
<path d="M19 17V5a2 2 0 0 0-2-2H4" />
|
||||||
|
<path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconInfo({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M12 16v-4" />
|
||||||
|
<path d="M12 8h.01" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconDownload({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M12 15V3" />
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<path d="m7 10 5 5 5-5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconTrash2({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M3 6h18" />
|
||||||
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
||||||
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||||
|
<line x1="10" x2="10" y1="11" y2="17" />
|
||||||
|
<line x1="14" x2="14" y1="11" y2="17" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconChevronUp({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="m18 15-6-6-6 6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconChevronDown({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="m6 9 6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconSearch({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="m21 21-4.34-4.34" />
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconX({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M18 6 6 18" />
|
||||||
|
<path d="m6 6 12 12" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconCheck({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconEye({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconEyeOff({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
|
||||||
|
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
||||||
|
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
|
||||||
|
<path d="m2 2 20 20" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconInbox({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
||||||
|
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconSatellite({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="m13.5 6.5-3.148-3.148a1.205 1.205 0 0 0-1.704 0L6.352 5.648a1.205 1.205 0 0 0 0 1.704L9.5 10.5" />
|
||||||
|
<path d="M16.5 7.5 19 5" />
|
||||||
|
<path d="m17.5 10.5 3.148 3.148a1.205 1.205 0 0 1 0 1.704l-2.296 2.296a1.205 1.205 0 0 1-1.704 0L13.5 14.5" />
|
||||||
|
<path d="M9 21a6 6 0 0 0-6-6" />
|
||||||
|
<path d="M9.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l4.296-4.296a1.205 1.205 0 0 0 0-1.704l-2.296-2.296a1.205 1.205 0 0 0-1.704 0z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconDiamond({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconTimer({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<line x1="10" x2="14" y1="2" y2="2" />
|
||||||
|
<line x1="12" x2="15" y1="14" y2="11" />
|
||||||
|
<circle cx="12" cy="14" r="8" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconTrendingUp({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<path d="M16 7h6v6" />
|
||||||
|
<path d="m22 7-8.5 8.5-5-5L2 17" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconDollarSign({ size = 20, ...props }: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg {...baseSvgProps} width={size} height={size} {...props}>
|
||||||
|
<line x1="12" x2="12" y1="2" y2="22" />
|
||||||
|
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -274,6 +274,10 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.apiKeyEntryStatSuccess {
|
.apiKeyEntryStatSuccess {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Modal } from '@/components/ui/Modal';
|
|||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { HeaderInputList } from '@/components/ui/HeaderInputList';
|
import { HeaderInputList } from '@/components/ui/HeaderInputList';
|
||||||
import { ModelInputList, modelsToEntries, entriesToModels } from '@/components/ui/ModelInputList';
|
import { ModelInputList, modelsToEntries, entriesToModels } from '@/components/ui/ModelInputList';
|
||||||
|
import { IconCheck, IconX } from '@/components/ui/icons';
|
||||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||||
import { modelsApi, providersApi, usageApi } from '@/services/api';
|
import { modelsApi, providersApi, usageApi } from '@/services/api';
|
||||||
import type {
|
import type {
|
||||||
@@ -1038,10 +1039,10 @@ export function AiProvidersPage() {
|
|||||||
)}
|
)}
|
||||||
<div className={styles.apiKeyEntryStats}>
|
<div className={styles.apiKeyEntryStats}>
|
||||||
<span className={`${styles.apiKeyEntryStat} ${styles.apiKeyEntryStatSuccess}`}>
|
<span className={`${styles.apiKeyEntryStat} ${styles.apiKeyEntryStatSuccess}`}>
|
||||||
✓ {entryStats.success}
|
<IconCheck size={12} /> {entryStats.success}
|
||||||
</span>
|
</span>
|
||||||
<span className={`${styles.apiKeyEntryStat} ${styles.apiKeyEntryStatFailure}`}>
|
<span className={`${styles.apiKeyEntryStat} ${styles.apiKeyEntryStatFailure}`}>
|
||||||
✗ {entryStats.failure}
|
<IconX size={12} /> {entryStats.failure}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -251,8 +251,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.actionIcon {
|
.actionIcon {
|
||||||
font-style: normal;
|
display: block;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtualBadge {
|
.virtualBadge {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
|
import { IconDownload, IconInfo, IconTrash2 } from '@/components/ui/icons';
|
||||||
import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores';
|
import { useAuthStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||||
import { authFilesApi, usageApi } from '@/services/api';
|
import { authFilesApi, usageApi } from '@/services/api';
|
||||||
import { apiClient } from '@/services/api/client';
|
import { apiClient } from '@/services/api/client';
|
||||||
@@ -545,30 +546,33 @@ export function AuthFilesPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => showDetails(item)}
|
onClick={() => showDetails(item)}
|
||||||
className={styles.iconButton}
|
className={styles.iconButton}
|
||||||
|
title={t('common.info', { defaultValue: '关于' })}
|
||||||
disabled={disableControls}
|
disabled={disableControls}
|
||||||
>
|
>
|
||||||
<i className={styles.actionIcon}>ℹ</i>
|
<IconInfo className={styles.actionIcon} size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDownload(item.name)}
|
onClick={() => handleDownload(item.name)}
|
||||||
className={styles.iconButton}
|
className={styles.iconButton}
|
||||||
|
title={t('auth_files.download_button')}
|
||||||
disabled={disableControls}
|
disabled={disableControls}
|
||||||
>
|
>
|
||||||
<i className={styles.actionIcon}>↓</i>
|
<IconDownload className={styles.actionIcon} size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDelete(item.name)}
|
onClick={() => handleDelete(item.name)}
|
||||||
className={styles.iconButton}
|
className={styles.iconButton}
|
||||||
|
title={t('auth_files.delete_button')}
|
||||||
disabled={disableControls || deleting === item.name}
|
disabled={disableControls || deleting === item.name}
|
||||||
>
|
>
|
||||||
{deleting === item.name ? (
|
{deleting === item.name ? (
|
||||||
<LoadingSpinner size={14} />
|
<LoadingSpinner size={14} />
|
||||||
) : (
|
) : (
|
||||||
<i className={styles.actionIcon}>🗑</i>
|
<IconTrash2 className={styles.actionIcon} size={16} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { keymap } from '@codemirror/view';
|
|||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { IconChevronDown, IconChevronUp, IconSearch } from '@/components/ui/icons';
|
||||||
import { useNotificationStore, useAuthStore, useThemeStore } from '@/stores';
|
import { useNotificationStore, useAuthStore, useThemeStore } from '@/stores';
|
||||||
import { configFileApi } from '@/services/api/configFile';
|
import { configFileApi } from '@/services/api/configFile';
|
||||||
import styles from './ConfigPage.module.scss';
|
import styles from './ConfigPage.module.scss';
|
||||||
@@ -256,20 +257,7 @@ export function ConfigPage() {
|
|||||||
disabled={!searchQuery || disableControls || loading}
|
disabled={!searchQuery || disableControls || loading}
|
||||||
title={t('config_management.search_button', { defaultValue: '搜索' })}
|
title={t('config_management.search_button', { defaultValue: '搜索' })}
|
||||||
>
|
>
|
||||||
<svg
|
<IconSearch size={16} />
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<circle cx="11" cy="11" r="8" />
|
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -283,7 +271,7 @@ export function ConfigPage() {
|
|||||||
disabled={!searchQuery || lastSearchedQuery !== searchQuery || searchResults.total === 0}
|
disabled={!searchQuery || lastSearchedQuery !== searchQuery || searchResults.total === 0}
|
||||||
title={t('config_management.search_prev', { defaultValue: '上一个' })}
|
title={t('config_management.search_prev', { defaultValue: '上一个' })}
|
||||||
>
|
>
|
||||||
↑
|
<IconChevronUp size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -292,7 +280,7 @@ export function ConfigPage() {
|
|||||||
disabled={!searchQuery || lastSearchedQuery !== searchQuery || searchResults.total === 0}
|
disabled={!searchQuery || lastSearchedQuery !== searchQuery || searchResults.total === 0}
|
||||||
title={t('config_management.search_next', { defaultValue: '下一个' })}
|
title={t('config_management.search_next', { defaultValue: '下一个' })}
|
||||||
>
|
>
|
||||||
↓
|
<IconChevronDown size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { IconEye, IconEyeOff } from '@/components/ui/icons';
|
||||||
import { useAuthStore, useNotificationStore } from '@/stores';
|
import { useAuthStore, useNotificationStore } from '@/stores';
|
||||||
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
|
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
|
||||||
|
|
||||||
@@ -121,8 +122,18 @@ export function LoginPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost btn-sm"
|
className="btn btn-ghost btn-sm"
|
||||||
onClick={() => setShowKey((prev) => !prev)}
|
onClick={() => setShowKey((prev) => !prev)}
|
||||||
|
aria-label={
|
||||||
|
showKey
|
||||||
|
? t('login.hide_key', { defaultValue: '隐藏密钥' })
|
||||||
|
: t('login.show_key', { defaultValue: '显示密钥' })
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
showKey
|
||||||
|
? t('login.hide_key', { defaultValue: '隐藏密钥' })
|
||||||
|
: t('login.show_key', { defaultValue: '显示密钥' })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{showKey ? '🙈' : '👁️'}
|
{showKey ? <IconEyeOff size={16} /> : <IconEye size={16} />}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -111,6 +111,10 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.statHeader {
|
.statHeader {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Line } from 'react-chartjs-2';
|
|||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { IconDiamond, IconDollarSign, IconSatellite, IconTimer, IconTrendingUp } from '@/components/ui/icons';
|
||||||
import { usageApi } from '@/services/api/usage';
|
import { usageApi } from '@/services/api/usage';
|
||||||
import {
|
import {
|
||||||
formatTokensInMillions,
|
formatTokensInMillions,
|
||||||
@@ -319,7 +320,7 @@ export function UsagePage() {
|
|||||||
{
|
{
|
||||||
key: 'requests',
|
key: 'requests',
|
||||||
label: t('usage_stats.total_requests'),
|
label: t('usage_stats.total_requests'),
|
||||||
icon: '🛰️',
|
icon: <IconSatellite size={16} />,
|
||||||
accent: '#2563eb',
|
accent: '#2563eb',
|
||||||
value: loading ? '-' : (usage?.total_requests ?? 0).toLocaleString(),
|
value: loading ? '-' : (usage?.total_requests ?? 0).toLocaleString(),
|
||||||
meta: (
|
meta: (
|
||||||
@@ -339,7 +340,7 @@ export function UsagePage() {
|
|||||||
{
|
{
|
||||||
key: 'tokens',
|
key: 'tokens',
|
||||||
label: t('usage_stats.total_tokens'),
|
label: t('usage_stats.total_tokens'),
|
||||||
icon: '💠',
|
icon: <IconDiamond size={16} />,
|
||||||
accent: '#8b5cf6',
|
accent: '#8b5cf6',
|
||||||
value: loading ? '-' : formatTokensInMillions(usage?.total_tokens ?? 0),
|
value: loading ? '-' : formatTokensInMillions(usage?.total_tokens ?? 0),
|
||||||
meta: (
|
meta: (
|
||||||
@@ -357,7 +358,7 @@ export function UsagePage() {
|
|||||||
{
|
{
|
||||||
key: 'rpm',
|
key: 'rpm',
|
||||||
label: t('usage_stats.rpm_30m'),
|
label: t('usage_stats.rpm_30m'),
|
||||||
icon: '⏱️',
|
icon: <IconTimer size={16} />,
|
||||||
accent: '#22c55e',
|
accent: '#22c55e',
|
||||||
value: loading ? '-' : formatPerMinuteValue(rateStats.rpm),
|
value: loading ? '-' : formatPerMinuteValue(rateStats.rpm),
|
||||||
meta: (
|
meta: (
|
||||||
@@ -370,7 +371,7 @@ export function UsagePage() {
|
|||||||
{
|
{
|
||||||
key: 'tpm',
|
key: 'tpm',
|
||||||
label: t('usage_stats.tpm_30m'),
|
label: t('usage_stats.tpm_30m'),
|
||||||
icon: '📈',
|
icon: <IconTrendingUp size={16} />,
|
||||||
accent: '#f97316',
|
accent: '#f97316',
|
||||||
value: loading ? '-' : formatPerMinuteValue(rateStats.tpm),
|
value: loading ? '-' : formatPerMinuteValue(rateStats.tpm),
|
||||||
meta: (
|
meta: (
|
||||||
@@ -383,7 +384,7 @@ export function UsagePage() {
|
|||||||
{
|
{
|
||||||
key: 'cost',
|
key: 'cost',
|
||||||
label: t('usage_stats.total_cost'),
|
label: t('usage_stats.total_cost'),
|
||||||
icon: '💰',
|
icon: <IconDollarSign size={16} />,
|
||||||
accent: '#f59e0b',
|
accent: '#f59e0b',
|
||||||
value: loading ? '-' : hasPrices ? formatUsd(totalCost) : '--',
|
value: loading ? '-' : hasPrices ? formatUsd(totalCost) : '--',
|
||||||
meta: (
|
meta: (
|
||||||
|
|||||||
@@ -229,14 +229,26 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
border-radius: $radius-md;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s ease;
|
transition: color 0.15s ease, background-color 0.15s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,12 +365,27 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
border-radius: $radius-md;
|
||||||
|
transition: color 0.15s ease, background-color 0.15s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +428,10 @@ textarea {
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-title {
|
.empty-title {
|
||||||
|
|||||||
Reference in New Issue
Block a user