mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 11:10:49 +08:00
feat: unify page layout with consistent page titles and structure
- Add page title (h1) to all main pages for consistent hierarchy - Wrap page content in container/content div structure - Handle 404 error for unsupported OAuth excluded models API - Add cache price input field in usage page model pricing - Add upgrade required i18n messages for older CPA versions - Import mixins in page-level SCSS modules
This commit is contained in:
@@ -370,7 +370,10 @@
|
|||||||
"load_failed": "Failed to load exclusion list",
|
"load_failed": "Failed to load exclusion list",
|
||||||
"provider_required": "Please enter a provider first",
|
"provider_required": "Please enter a provider first",
|
||||||
"scope_all": "Scope: All providers",
|
"scope_all": "Scope: All providers",
|
||||||
"scope_provider": "Scope: {provider}"
|
"scope_provider": "Scope: {provider}",
|
||||||
|
"upgrade_required": "This feature requires a newer CLI Proxy API (CPA) version. Please upgrade.",
|
||||||
|
"upgrade_required_title": "Please upgrade CLI Proxy API",
|
||||||
|
"upgrade_required_desc": "The current server does not support the OAuth excluded models API. Please upgrade to the latest CLI Proxy API (CPA) version."
|
||||||
},
|
},
|
||||||
"auth_login": {
|
"auth_login": {
|
||||||
"codex_oauth_title": "Codex OAuth",
|
"codex_oauth_title": "Codex OAuth",
|
||||||
|
|||||||
@@ -380,7 +380,10 @@
|
|||||||
"load_failed": "加载排除列表失败",
|
"load_failed": "加载排除列表失败",
|
||||||
"provider_required": "请先填写提供商名称",
|
"provider_required": "请先填写提供商名称",
|
||||||
"scope_all": "当前范围:全局(显示所有提供商)",
|
"scope_all": "当前范围:全局(显示所有提供商)",
|
||||||
"scope_provider": "当前范围:{{provider}}"
|
"scope_provider": "当前范围:{{provider}}",
|
||||||
|
"upgrade_required": "当前 CPA 版本不支持模型排除列表,请升级 CPA 版本",
|
||||||
|
"upgrade_required_title": "需要升级 CPA 版本",
|
||||||
|
"upgrade_required_desc": "当前服务器版本不支持获取模型排除列表功能,请升级到最新版本的 CPA(CLI Proxy API)后重试。"
|
||||||
},
|
},
|
||||||
"auth_login": {
|
"auth_login": {
|
||||||
"codex_oauth_title": "Codex OAuth",
|
"codex_oauth_title": "Codex OAuth",
|
||||||
|
|||||||
@@ -1072,8 +1072,10 @@ export function AiProvidersPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className={styles.container}>
|
||||||
{error && <div className="error-box">{error}</div>}
|
<h1 className={styles.pageTitle}>{t('ai_providers.title')}</h1>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{error && <div className="error-box">{error}</div>}
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title={t('ai_providers.gemini_title')}
|
title={t('ai_providers.gemini_title')}
|
||||||
@@ -1972,6 +1974,7 @@ export function AiProvidersPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use '../styles/mixins' as *;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|||||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||||
import { apiKeysApi } from '@/services/api';
|
import { apiKeysApi } from '@/services/api';
|
||||||
import { maskApiKey } from '@/utils/format';
|
import { maskApiKey } from '@/utils/format';
|
||||||
|
import styles from './ApiKeysPage.module.scss';
|
||||||
|
|
||||||
export function ApiKeysPage() {
|
export function ApiKeysPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -138,80 +139,84 @@ export function ApiKeysPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('api_keys.proxy_auth_title')} extra={actionButtons}>
|
<div className={styles.container}>
|
||||||
{error && <div className="error-box">{error}</div>}
|
<h1 className={styles.pageTitle}>{t('api_keys.title')}</h1>
|
||||||
|
|
||||||
{loading ? (
|
<Card title={t('api_keys.proxy_auth_title')} extra={actionButtons}>
|
||||||
<div className="flex-center" style={{ padding: '24px 0' }}>
|
{error && <div className="error-box">{error}</div>}
|
||||||
<LoadingSpinner size={28} />
|
|
||||||
</div>
|
|
||||||
) : apiKeys.length === 0 ? (
|
|
||||||
<EmptyState
|
|
||||||
title={t('api_keys.empty_title')}
|
|
||||||
description={t('api_keys.empty_desc')}
|
|
||||||
action={
|
|
||||||
<Button onClick={openAddModal} disabled={disableControls}>
|
|
||||||
{t('api_keys.add_button')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="item-list">
|
|
||||||
{apiKeys.map((key, index) => (
|
|
||||||
<div key={index} className="item-row">
|
|
||||||
<div className="item-meta">
|
|
||||||
<div className="pill">#{index + 1}</div>
|
|
||||||
<div className="item-title">{t('api_keys.item_title')}</div>
|
|
||||||
<div className="item-subtitle">{maskApiKey(String(key || ''))}</div>
|
|
||||||
</div>
|
|
||||||
<div className="item-actions">
|
|
||||||
<Button variant="secondary" size="sm" onClick={() => openEditModal(index)} disabled={disableControls}>
|
|
||||||
{t('common.edit')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDelete(index)}
|
|
||||||
disabled={disableControls || deletingIndex === index}
|
|
||||||
loading={deletingIndex === index}
|
|
||||||
>
|
|
||||||
{t('common.delete')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Modal
|
{loading ? (
|
||||||
open={modalOpen}
|
<div className="flex-center" style={{ padding: '24px 0' }}>
|
||||||
onClose={closeModal}
|
<LoadingSpinner size={28} />
|
||||||
title={editingIndex !== null ? t('api_keys.edit_modal_title') : t('api_keys.add_modal_title')}
|
</div>
|
||||||
footer={
|
) : apiKeys.length === 0 ? (
|
||||||
<>
|
<EmptyState
|
||||||
<Button variant="secondary" onClick={closeModal} disabled={saving}>
|
title={t('api_keys.empty_title')}
|
||||||
{t('common.cancel')}
|
description={t('api_keys.empty_desc')}
|
||||||
</Button>
|
action={
|
||||||
<Button onClick={handleSave} loading={saving}>
|
<Button onClick={openAddModal} disabled={disableControls}>
|
||||||
{editingIndex !== null ? t('common.update') : t('common.add')}
|
{t('api_keys.add_button')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
}
|
||||||
}
|
/>
|
||||||
>
|
) : (
|
||||||
<Input
|
<div className="item-list">
|
||||||
label={
|
{apiKeys.map((key, index) => (
|
||||||
editingIndex !== null ? t('api_keys.edit_modal_key_label') : t('api_keys.add_modal_key_label')
|
<div key={index} className="item-row">
|
||||||
|
<div className="item-meta">
|
||||||
|
<div className="pill">#{index + 1}</div>
|
||||||
|
<div className="item-title">{t('api_keys.item_title')}</div>
|
||||||
|
<div className="item-subtitle">{maskApiKey(String(key || ''))}</div>
|
||||||
|
</div>
|
||||||
|
<div className="item-actions">
|
||||||
|
<Button variant="secondary" size="sm" onClick={() => openEditModal(index)} disabled={disableControls}>
|
||||||
|
{t('common.edit')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(index)}
|
||||||
|
disabled={disableControls || deletingIndex === index}
|
||||||
|
loading={deletingIndex === index}
|
||||||
|
>
|
||||||
|
{t('common.delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={modalOpen}
|
||||||
|
onClose={closeModal}
|
||||||
|
title={editingIndex !== null ? t('api_keys.edit_modal_title') : t('api_keys.add_modal_title')}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="secondary" onClick={closeModal} disabled={saving}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} loading={saving}>
|
||||||
|
{editingIndex !== null ? t('common.update') : t('common.add')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
placeholder={
|
>
|
||||||
editingIndex !== null
|
<Input
|
||||||
? t('api_keys.edit_modal_key_label')
|
label={
|
||||||
: t('api_keys.add_modal_key_placeholder')
|
editingIndex !== null ? t('api_keys.edit_modal_key_label') : t('api_keys.add_modal_key_label')
|
||||||
}
|
}
|
||||||
value={inputValue}
|
placeholder={
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
editingIndex !== null
|
||||||
disabled={saving}
|
? t('api_keys.edit_modal_key_label')
|
||||||
/>
|
: t('api_keys.add_modal_key_placeholder')
|
||||||
</Modal>
|
}
|
||||||
</Card>
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,25 @@
|
|||||||
gap: $spacing-lg;
|
gap: $spacing-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pageHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.headerActions {
|
.headerActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-sm;
|
gap: $spacing-sm;
|
||||||
@@ -472,4 +491,3 @@
|
|||||||
border: 1px solid var(--danger-color);
|
border: 1px solid var(--danger-color);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,11 +150,13 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
// OAuth 排除模型相关
|
// OAuth 排除模型相关
|
||||||
const [excluded, setExcluded] = useState<Record<string, string[]>>({});
|
const [excluded, setExcluded] = useState<Record<string, string[]>>({});
|
||||||
|
const [excludedError, setExcludedError] = useState<'unsupported' | null>(null);
|
||||||
const [excludedModalOpen, setExcludedModalOpen] = useState(false);
|
const [excludedModalOpen, setExcludedModalOpen] = useState(false);
|
||||||
const [excludedForm, setExcludedForm] = useState<ExcludedFormState>({ provider: '', modelsText: '' });
|
const [excludedForm, setExcludedForm] = useState<ExcludedFormState>({ provider: '', modelsText: '' });
|
||||||
const [savingExcluded, setSavingExcluded] = useState(false);
|
const [savingExcluded, setSavingExcluded] = useState(false);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const excludedUnsupportedRef = useRef(false);
|
||||||
|
|
||||||
const disableControls = connectionStatus !== 'connected';
|
const disableControls = connectionStatus !== 'connected';
|
||||||
|
|
||||||
@@ -199,11 +201,27 @@ export function AuthFilesPage() {
|
|||||||
const loadExcluded = useCallback(async () => {
|
const loadExcluded = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await authFilesApi.getOauthExcludedModels();
|
const res = await authFilesApi.getOauthExcludedModels();
|
||||||
|
excludedUnsupportedRef.current = false;
|
||||||
setExcluded(res || {});
|
setExcluded(res || {});
|
||||||
} catch {
|
setExcludedError(null);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const status =
|
||||||
|
typeof err === 'object' && err !== null && 'status' in err
|
||||||
|
? (err as { status?: unknown }).status
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
setExcluded({});
|
||||||
|
setExcludedError('unsupported');
|
||||||
|
if (!excludedUnsupportedRef.current) {
|
||||||
|
excludedUnsupportedRef.current = true;
|
||||||
|
showNotification(t('oauth_excluded.upgrade_required'), 'warning');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 静默失败
|
// 静默失败
|
||||||
}
|
}
|
||||||
}, []);
|
}, [showNotification, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFiles();
|
loadFiles();
|
||||||
@@ -651,8 +669,13 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
<div className={styles.pageHeader}>
|
||||||
|
<h1 className={styles.pageTitle}>{t('auth_files.title')}</h1>
|
||||||
|
<p className={styles.description}>{t('auth_files.description')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title={t('auth_files.title')}
|
title={t('auth_files.title_section')}
|
||||||
extra={
|
extra={
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<Button variant="secondary" size="sm" onClick={() => { loadFiles(); loadKeyStats(); }} disabled={loading}>
|
<Button variant="secondary" size="sm" onClick={() => { loadFiles(); loadKeyStats(); }} disabled={loading}>
|
||||||
@@ -770,12 +793,21 @@ export function AuthFilesPage() {
|
|||||||
<Card
|
<Card
|
||||||
title={t('oauth_excluded.title')}
|
title={t('oauth_excluded.title')}
|
||||||
extra={
|
extra={
|
||||||
<Button size="sm" onClick={() => openExcludedModal()} disabled={disableControls}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => openExcludedModal()}
|
||||||
|
disabled={disableControls || excludedError === 'unsupported'}
|
||||||
|
>
|
||||||
{t('oauth_excluded.add')}
|
{t('oauth_excluded.add')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{Object.keys(excluded).length === 0 ? (
|
{excludedError === 'unsupported' ? (
|
||||||
|
<EmptyState
|
||||||
|
title={t('oauth_excluded.upgrade_required_title')}
|
||||||
|
description={t('oauth_excluded.upgrade_required_desc')}
|
||||||
|
/>
|
||||||
|
) : Object.keys(excluded).length === 0 ? (
|
||||||
<EmptyState title={t('oauth_excluded.list_empty_all')} />
|
<EmptyState title={t('oauth_excluded.list_empty_all')} />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.excludedList}>
|
<div className={styles.excludedList}>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use '../styles/mixins' as *;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { useNotificationStore, useAuthStore } from '@/stores';
|
import { useNotificationStore, useAuthStore } from '@/stores';
|
||||||
import { logsApi } from '@/services/api/logs';
|
import { logsApi } from '@/services/api/logs';
|
||||||
|
import styles from './LogsPage.module.scss';
|
||||||
|
|
||||||
interface ErrorLogItem {
|
interface ErrorLogItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -197,9 +198,11 @@ export function LogsPage() {
|
|||||||
const logsText = logLines.join('\n');
|
const logsText = logLines.join('\n');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className={styles.container}>
|
||||||
|
<h1 className={styles.pageTitle}>{t('logs.title')}</h1>
|
||||||
|
<div className={styles.content}>
|
||||||
<Card
|
<Card
|
||||||
title={t('logs.title')}
|
title={t('logs.log_content')}
|
||||||
extra={
|
extra={
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
<Button variant="secondary" size="sm" onClick={() => loadLogs(false)} disabled={loading}>
|
<Button variant="secondary" size="sm" onClick={() => loadLogs(false)} disabled={loading}>
|
||||||
@@ -264,6 +267,7 @@ export function LogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use '../styles/mixins' as *;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { useNotificationStore } from '@/stores';
|
import { useNotificationStore } from '@/stores';
|
||||||
import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth';
|
import { oauthApi, type OAuthProvider, type IFlowCookieAuthResponse } from '@/services/api/oauth';
|
||||||
import { isLocalhost } from '@/utils/connection';
|
import { isLocalhost } from '@/utils/connection';
|
||||||
|
import styles from './OAuthPage.module.scss';
|
||||||
|
|
||||||
interface ProviderState {
|
interface ProviderState {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -157,130 +158,134 @@ export function OAuthPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className={styles.container}>
|
||||||
{PROVIDERS.map((provider) => {
|
<h1 className={styles.pageTitle}>{t('nav.oauth', { defaultValue: 'OAuth' })}</h1>
|
||||||
const state = states[provider.id] || {};
|
|
||||||
// 非本地访问时禁用所有 OAuth 登录方式
|
|
||||||
const isDisabled = !isLocal;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={provider.id}
|
|
||||||
style={isDisabled ? { opacity: 0.6, pointerEvents: 'none' } : undefined}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
title={t(provider.titleKey)}
|
|
||||||
extra={
|
|
||||||
<Button
|
|
||||||
onClick={() => startAuth(provider.id)}
|
|
||||||
loading={state.polling}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
{t('common.login')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="hint">{t(provider.hintKey)}</div>
|
|
||||||
{isDisabled && (
|
|
||||||
<div className="status-badge warning" style={{ marginTop: 8 }}>
|
|
||||||
{t('auth_login.remote_access_disabled')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isDisabled && state.url && (
|
|
||||||
<div className="connection-box">
|
|
||||||
<div className="label">{t(provider.urlLabelKey)}</div>
|
|
||||||
<div className="value">{state.url}</div>
|
|
||||||
<div className="item-actions" style={{ marginTop: 8 }}>
|
|
||||||
<Button variant="secondary" size="sm" onClick={() => copyLink(state.url!)}>
|
|
||||||
{t('auth_login.codex_copy_link')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => window.open(state.url, '_blank', 'noopener,noreferrer')}
|
|
||||||
>
|
|
||||||
{t('auth_login.codex_open_link')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isDisabled && state.status && state.status !== 'idle' && (
|
|
||||||
<div className="status-badge" style={{ marginTop: 8 }}>
|
|
||||||
{state.status === 'success'
|
|
||||||
? t('auth_login.codex_oauth_status_success')
|
|
||||||
: state.status === 'error'
|
|
||||||
? `${t('auth_login.codex_oauth_status_error')} ${state.error || ''}`
|
|
||||||
: t('auth_login.codex_oauth_status_waiting')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* iFlow Cookie 登录 */}
|
<div className={styles.content}>
|
||||||
<Card
|
{PROVIDERS.map((provider) => {
|
||||||
title={t('auth_login.iflow_cookie_title')}
|
const state = states[provider.id] || {};
|
||||||
extra={
|
// 非本地访问时禁用所有 OAuth 登录方式
|
||||||
<Button onClick={submitIflowCookie} loading={iflowCookie.loading}>
|
const isDisabled = !isLocal;
|
||||||
{t('auth_login.iflow_cookie_button')}
|
return (
|
||||||
</Button>
|
<div
|
||||||
}
|
key={provider.id}
|
||||||
>
|
style={isDisabled ? { opacity: 0.6, pointerEvents: 'none' } : undefined}
|
||||||
<div className="hint">{t('auth_login.iflow_cookie_hint')}</div>
|
>
|
||||||
<div className="hint" style={{ marginTop: 4 }}>
|
<Card
|
||||||
{t('auth_login.iflow_cookie_key_hint')}
|
title={t(provider.titleKey)}
|
||||||
</div>
|
extra={
|
||||||
<div className="form-item" style={{ marginTop: 12 }}>
|
<Button
|
||||||
<label className="label">{t('auth_login.iflow_cookie_label')}</label>
|
onClick={() => startAuth(provider.id)}
|
||||||
<Input
|
loading={state.polling}
|
||||||
value={iflowCookie.cookie}
|
disabled={isDisabled}
|
||||||
onChange={(e) => setIflowCookie((prev) => ({ ...prev, cookie: e.target.value }))}
|
>
|
||||||
placeholder={t('auth_login.iflow_cookie_placeholder')}
|
{t('common.login')}
|
||||||
/>
|
</Button>
|
||||||
</div>
|
}
|
||||||
{iflowCookie.error && (
|
>
|
||||||
<div
|
<div className="hint">{t(provider.hintKey)}</div>
|
||||||
className={`status-badge ${iflowCookie.errorType === 'warning' ? 'warning' : 'error'}`}
|
{isDisabled && (
|
||||||
style={{ marginTop: 8 }}
|
<div className="status-badge warning" style={{ marginTop: 8 }}>
|
||||||
>
|
{t('auth_login.remote_access_disabled')}
|
||||||
{iflowCookie.errorType === 'warning'
|
</div>
|
||||||
? t('auth_login.iflow_cookie_status_duplicate')
|
)}
|
||||||
: t('auth_login.iflow_cookie_status_error')}{' '}
|
{!isDisabled && state.url && (
|
||||||
{iflowCookie.error}
|
<div className="connection-box">
|
||||||
</div>
|
<div className="label">{t(provider.urlLabelKey)}</div>
|
||||||
)}
|
<div className="value">{state.url}</div>
|
||||||
{iflowCookie.result && iflowCookie.result.status === 'ok' && (
|
<div className="item-actions" style={{ marginTop: 8 }}>
|
||||||
<div className="connection-box" style={{ marginTop: 12 }}>
|
<Button variant="secondary" size="sm" onClick={() => copyLink(state.url!)}>
|
||||||
<div className="label">{t('auth_login.iflow_cookie_result_title')}</div>
|
{t('auth_login.codex_copy_link')}
|
||||||
<div className="key-value-list">
|
</Button>
|
||||||
{iflowCookie.result.email && (
|
<Button
|
||||||
<div className="key-value-item">
|
variant="secondary"
|
||||||
<span className="key">{t('auth_login.iflow_cookie_result_email')}</span>
|
size="sm"
|
||||||
<span className="value">{iflowCookie.result.email}</span>
|
onClick={() => window.open(state.url, '_blank', 'noopener,noreferrer')}
|
||||||
</div>
|
>
|
||||||
)}
|
{t('auth_login.codex_open_link')}
|
||||||
{iflowCookie.result.expired && (
|
</Button>
|
||||||
<div className="key-value-item">
|
</div>
|
||||||
<span className="key">{t('auth_login.iflow_cookie_result_expired')}</span>
|
</div>
|
||||||
<span className="value">{iflowCookie.result.expired}</span>
|
)}
|
||||||
</div>
|
{!isDisabled && state.status && state.status !== 'idle' && (
|
||||||
)}
|
<div className="status-badge" style={{ marginTop: 8 }}>
|
||||||
{iflowCookie.result.saved_path && (
|
{state.status === 'success'
|
||||||
<div className="key-value-item">
|
? t('auth_login.codex_oauth_status_success')
|
||||||
<span className="key">{t('auth_login.iflow_cookie_result_path')}</span>
|
: state.status === 'error'
|
||||||
<span className="value">{iflowCookie.result.saved_path}</span>
|
? `${t('auth_login.codex_oauth_status_error')} ${state.error || ''}`
|
||||||
</div>
|
: t('auth_login.codex_oauth_status_waiting')}
|
||||||
)}
|
</div>
|
||||||
{iflowCookie.result.type && (
|
)}
|
||||||
<div className="key-value-item">
|
</Card>
|
||||||
<span className="key">{t('auth_login.iflow_cookie_result_type')}</span>
|
|
||||||
<span className="value">{iflowCookie.result.type}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* iFlow Cookie 登录 */}
|
||||||
|
<Card
|
||||||
|
title={t('auth_login.iflow_cookie_title')}
|
||||||
|
extra={
|
||||||
|
<Button onClick={submitIflowCookie} loading={iflowCookie.loading}>
|
||||||
|
{t('auth_login.iflow_cookie_button')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="hint">{t('auth_login.iflow_cookie_hint')}</div>
|
||||||
|
<div className="hint" style={{ marginTop: 4 }}>
|
||||||
|
{t('auth_login.iflow_cookie_key_hint')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="form-item" style={{ marginTop: 12 }}>
|
||||||
</Card>
|
<label className="label">{t('auth_login.iflow_cookie_label')}</label>
|
||||||
|
<Input
|
||||||
|
value={iflowCookie.cookie}
|
||||||
|
onChange={(e) => setIflowCookie((prev) => ({ ...prev, cookie: e.target.value }))}
|
||||||
|
placeholder={t('auth_login.iflow_cookie_placeholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{iflowCookie.error && (
|
||||||
|
<div
|
||||||
|
className={`status-badge ${iflowCookie.errorType === 'warning' ? 'warning' : 'error'}`}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
>
|
||||||
|
{iflowCookie.errorType === 'warning'
|
||||||
|
? t('auth_login.iflow_cookie_status_duplicate')
|
||||||
|
: t('auth_login.iflow_cookie_status_error')}{' '}
|
||||||
|
{iflowCookie.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result && iflowCookie.result.status === 'ok' && (
|
||||||
|
<div className="connection-box" style={{ marginTop: 12 }}>
|
||||||
|
<div className="label">{t('auth_login.iflow_cookie_result_title')}</div>
|
||||||
|
<div className="key-value-list">
|
||||||
|
{iflowCookie.result.email && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_email')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.email}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result.expired && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_expired')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.expired}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result.saved_path && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_path')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.saved_path}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{iflowCookie.result.type && (
|
||||||
|
<div className="key-value-item">
|
||||||
|
<span className="key">{t('auth_login.iflow_cookie_result_type')}</span>
|
||||||
|
<span className="value">{iflowCookie.result.type}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use '../../styles/mixins' as *;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||||
import { configApi } from '@/services/api';
|
import { configApi } from '@/services/api';
|
||||||
import type { Config } from '@/types';
|
import type { Config } from '@/types';
|
||||||
|
import styles from './Settings/Settings.module.scss';
|
||||||
|
|
||||||
type PendingKey =
|
type PendingKey =
|
||||||
| 'debug'
|
| 'debug'
|
||||||
@@ -168,8 +169,11 @@ export function SettingsPage() {
|
|||||||
const quotaSwitchPreview = config?.quotaExceeded?.switchPreviewModel ?? false;
|
const quotaSwitchPreview = config?.quotaExceeded?.switchPreviewModel ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid cols-2">
|
<div className={styles.container}>
|
||||||
<Card title={t('basic_settings.title')}>
|
<h1 className={styles.pageTitle}>{t('basic_settings.title')}</h1>
|
||||||
|
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<Card>
|
||||||
{error && <div className="error-box">{error}</div>}
|
{error && <div className="error-box">{error}</div>}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@@ -326,6 +330,7 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,9 +115,11 @@ export function SystemPage() {
|
|||||||
}, [auth.connectionStatus, auth.apiBase]);
|
}, [auth.connectionStatus, auth.apiBase]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className={styles.container}>
|
||||||
|
<h1 className={styles.pageTitle}>{t('system_info.title')}</h1>
|
||||||
|
<div className={styles.content}>
|
||||||
<Card
|
<Card
|
||||||
title={t('system_info.title')}
|
title={t('system_info.connection_status_title')}
|
||||||
extra={
|
extra={
|
||||||
<Button variant="secondary" size="sm" onClick={() => fetchConfig(undefined, true)}>
|
<Button variant="secondary" size="sm" onClick={() => fetchConfig(undefined, true)}>
|
||||||
{t('common.refresh')}
|
{t('common.refresh')}
|
||||||
@@ -186,6 +188,7 @@ export function SystemPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export function UsagePage() {
|
|||||||
const [selectedModel, setSelectedModel] = useState('');
|
const [selectedModel, setSelectedModel] = useState('');
|
||||||
const [promptPrice, setPromptPrice] = useState('');
|
const [promptPrice, setPromptPrice] = useState('');
|
||||||
const [completionPrice, setCompletionPrice] = useState('');
|
const [completionPrice, setCompletionPrice] = useState('');
|
||||||
|
const [cachePrice, setCachePrice] = useState('');
|
||||||
|
|
||||||
// Expanded sections
|
// Expanded sections
|
||||||
const [expandedApis, setExpandedApis] = useState<Set<string>>(new Set());
|
const [expandedApis, setExpandedApis] = useState<Set<string>>(new Set());
|
||||||
@@ -340,12 +341,14 @@ export function UsagePage() {
|
|||||||
if (!selectedModel) return;
|
if (!selectedModel) return;
|
||||||
const prompt = parseFloat(promptPrice) || 0;
|
const prompt = parseFloat(promptPrice) || 0;
|
||||||
const completion = parseFloat(completionPrice) || 0;
|
const completion = parseFloat(completionPrice) || 0;
|
||||||
const newPrices = { ...modelPrices, [selectedModel]: { prompt, completion } };
|
const cache = cachePrice.trim() === '' ? prompt : parseFloat(cachePrice) || 0;
|
||||||
|
const newPrices = { ...modelPrices, [selectedModel]: { prompt, completion, cache } };
|
||||||
setModelPrices(newPrices);
|
setModelPrices(newPrices);
|
||||||
saveModelPrices(newPrices);
|
saveModelPrices(newPrices);
|
||||||
setSelectedModel('');
|
setSelectedModel('');
|
||||||
setPromptPrice('');
|
setPromptPrice('');
|
||||||
setCompletionPrice('');
|
setCompletionPrice('');
|
||||||
|
setCachePrice('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle model price delete
|
// Handle model price delete
|
||||||
@@ -362,6 +365,7 @@ export function UsagePage() {
|
|||||||
setSelectedModel(model);
|
setSelectedModel(model);
|
||||||
setPromptPrice(price?.prompt?.toString() || '');
|
setPromptPrice(price?.prompt?.toString() || '');
|
||||||
setCompletionPrice(price?.completion?.toString() || '');
|
setCompletionPrice(price?.completion?.toString() || '');
|
||||||
|
setCachePrice(price?.cache?.toString() || '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle API expansion
|
// Toggle API expansion
|
||||||
@@ -771,9 +775,11 @@ export function UsagePage() {
|
|||||||
if (price) {
|
if (price) {
|
||||||
setPromptPrice(price.prompt.toString());
|
setPromptPrice(price.prompt.toString());
|
||||||
setCompletionPrice(price.completion.toString());
|
setCompletionPrice(price.completion.toString());
|
||||||
|
setCachePrice(price.cache.toString());
|
||||||
} else {
|
} else {
|
||||||
setPromptPrice('');
|
setPromptPrice('');
|
||||||
setCompletionPrice('');
|
setCompletionPrice('');
|
||||||
|
setCachePrice('');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={styles.select}
|
className={styles.select}
|
||||||
@@ -804,6 +810,16 @@ export function UsagePage() {
|
|||||||
step="0.0001"
|
step="0.0001"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.formField}>
|
||||||
|
<label>{t('usage_stats.model_price_cache')} ($/1M)</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={cachePrice}
|
||||||
|
onChange={(e) => setCachePrice(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
step="0.0001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleSavePrice}
|
onClick={handleSavePrice}
|
||||||
@@ -826,6 +842,7 @@ export function UsagePage() {
|
|||||||
<div className={styles.priceMeta}>
|
<div className={styles.priceMeta}>
|
||||||
<span>{t('usage_stats.model_price_prompt')}: ${price.prompt.toFixed(4)}/1M</span>
|
<span>{t('usage_stats.model_price_prompt')}: ${price.prompt.toFixed(4)}/1M</span>
|
||||||
<span>{t('usage_stats.model_price_completion')}: ${price.completion.toFixed(4)}/1M</span>
|
<span>{t('usage_stats.model_price_completion')}: ${price.completion.toFixed(4)}/1M</span>
|
||||||
|
<span>{t('usage_stats.model_price_cache')}: ${price.cache.toFixed(4)}/1M</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.priceActions}>
|
<div className={styles.priceActions}>
|
||||||
|
|||||||
Reference in New Issue
Block a user