Merge pull request #12 from router-for-me/main

sync: merge upstream router-for-me/Cli-Proxy-API-Management-Center

主要更新:
- 登录页面重新设计(分屏布局 + 自动登录 UI)
- AI 提供商编辑改为独立页面(替代弹窗模式)
- 新增浮动导航侧边栏,支持快速跳转
- OAuth 模型别名和排除模型编辑页面
- 新增多个模型图标(Codex、DeepSeek、GLM、Grok、Kimi、MiniMax)
- 页面过渡动画优化,防止空白闪烁
- 日志页面新增原始日志显示开关
- 移动端响应式布局优化
- 配额管理新增认证类型统计

冲突解决:
- src/stores/index.ts: 保留本地 useDisabledModelsStore 和 PR 的 useOpenAIEditDraftStore
- src/pages/AiProvidersPage.tsx: 采用 PR 版本(页面导航重构)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
kongkongyo
2026-02-02 21:38:04 +08:00
69 changed files with 5865 additions and 1700 deletions

View File

@@ -3,15 +3,39 @@ import { useTranslation } from 'react-i18next';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { IconGithub, IconBookOpen, IconExternalLink, IconCode } from '@/components/ui/icons';
import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore } from '@/stores';
import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore, useThemeStore } from '@/stores';
import { apiKeysApi } from '@/services/api/apiKeys';
import { classifyModels } from '@/utils/models';
import { STORAGE_KEY_AUTH } from '@/utils/constants';
import iconGemini from '@/assets/icons/gemini.svg';
import iconClaude from '@/assets/icons/claude.svg';
import iconOpenaiLight from '@/assets/icons/openai-light.svg';
import iconOpenaiDark from '@/assets/icons/openai-dark.svg';
import iconQwen from '@/assets/icons/qwen.svg';
import iconKimiLight from '@/assets/icons/kimi-light.svg';
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
import iconGlm from '@/assets/icons/glm.svg';
import iconGrok from '@/assets/icons/grok.svg';
import iconDeepseek from '@/assets/icons/deepseek.svg';
import iconMinimax from '@/assets/icons/minimax.svg';
import styles from './SystemPage.module.scss';
const MODEL_CATEGORY_ICONS: Record<string, string | { light: string; dark: string }> = {
gpt: { light: iconOpenaiLight, dark: iconOpenaiDark },
claude: iconClaude,
gemini: iconGemini,
qwen: iconQwen,
kimi: { light: iconKimiLight, dark: iconKimiDark },
glm: iconGlm,
grok: iconGrok,
deepseek: iconDeepseek,
minimax: iconMinimax,
};
export function SystemPage() {
const { t, i18n } = useTranslation();
const { showNotification, showConfirmation } = useNotificationStore();
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const auth = useAuthStore();
const config = useConfigStore((state) => state.config);
const fetchConfig = useConfigStore((state) => state.fetchConfig);
@@ -31,6 +55,13 @@ export function SystemPage() {
);
const groupedModels = useMemo(() => classifyModels(models, { otherLabel }), [models, otherLabel]);
const getIconForCategory = (categoryId: string): string | null => {
const iconEntry = MODEL_CATEGORY_ICONS[categoryId];
if (!iconEntry) return null;
if (typeof iconEntry === 'string') return iconEntry;
return resolvedTheme === 'dark' ? iconEntry.dark : iconEntry.light;
};
const normalizeApiKeyList = (input: any): string[] => {
if (!Array.isArray(input)) return [];
const seen = new Set<string>();
@@ -242,26 +273,32 @@ export function SystemPage() {
<div className="hint">{t('system_info.models_empty')}</div>
) : (
<div className="item-list">
{groupedModels.map((group) => (
<div key={group.id} className="item-row">
<div className="item-meta">
<div className="item-title">{group.label}</div>
<div className="item-subtitle">{t('system_info.models_count', { count: group.items.length })}</div>
{groupedModels.map((group) => {
const iconSrc = getIconForCategory(group.id);
return (
<div key={group.id} className="item-row">
<div className="item-meta">
<div className={styles.groupTitle}>
{iconSrc && <img src={iconSrc} alt="" className={styles.groupIcon} />}
<span className="item-title">{group.label}</span>
</div>
<div className="item-subtitle">{t('system_info.models_count', { count: group.items.length })}</div>
</div>
<div className={styles.modelTags}>
{group.items.map((model) => (
<span
key={`${model.name}-${model.alias ?? 'default'}`}
className={styles.modelTag}
title={model.description || ''}
>
<span className={styles.modelName}>{model.name}</span>
{model.alias && <span className={styles.modelAlias}>{model.alias}</span>}
</span>
))}
</div>
</div>
<div className={styles.modelTags}>
{group.items.map((model) => (
<span
key={`${model.name}-${model.alias ?? 'default'}`}
className={styles.modelTag}
title={model.description || ''}
>
<span className={styles.modelName}>{model.name}</span>
{model.alias && <span className={styles.modelAlias}>{model.alias}</span>}
</span>
))}
</div>
</div>
))}
);
})}
</div>
)}
</Card>