From d8eac39f9a672127330a0f623b62fc7453ea92e4 Mon Sep 17 00:00:00 2001 From: abingyyds Date: Tue, 7 Apr 2026 00:48:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=88=86=E7=AB=99=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E9=80=8F=E6=98=8E=E8=83=8C=E6=99=AF=E6=94=B9=E4=B8=BA=E4=B8=8D?= =?UTF-8?q?=E9=80=8F=E6=98=8E=20+=20=E5=88=A0=E9=99=A4=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 .modal-overlay CSS 类,为暗色/亮色/claude 主题提供不透明弹窗背景 - 所有弹窗(Topup/Tokens/Packages)添加 modal-overlay 类 - 从 Tokens 页面移除 ConfigExporter 组件及其源文件 Co-Authored-By: Claude Sonnet 4.6 --- src/components/ConfigExporter.jsx | 406 ------------------------------ src/index.css | 16 ++ src/pages/Packages.jsx | 2 +- src/pages/Tokens.jsx | 12 +- src/pages/Topup.jsx | 2 +- 5 files changed, 21 insertions(+), 417 deletions(-) delete mode 100644 src/components/ConfigExporter.jsx diff --git a/src/components/ConfigExporter.jsx b/src/components/ConfigExporter.jsx deleted file mode 100644 index b80e076..0000000 --- a/src/components/ConfigExporter.jsx +++ /dev/null @@ -1,406 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { getTokens, getSiteModels } from '../api'; -import toast from 'react-hot-toast'; - -const ConfigExporter = () => { - const { t } = useTranslation(); - const [tokens, setTokens] = useState([]); - const [selectedToken, setSelectedToken] = useState(null); - const [availableModels, setAvailableModels] = useState([]); - const [selectedModel, setSelectedModel] = useState(''); - const [selectedTool, setSelectedTool] = useState('claudecode'); - const [loading, setLoading] = useState(true); - const [copied, setCopied] = useState(false); - const [modelSearch, setModelSearch] = useState(''); - const [modelDropdownOpen, setModelDropdownOpen] = useState(false); - const modelDropdownRef = useRef(null); - - const serverAddress = window.location.origin; - - const tools = [ - { id: 'claudecode', name: 'Claude Code', path: '~/.claude/settings.json' }, - { id: 'openclaw', name: 'OpenClaw', path: '~/.openclaw/openclaw.json' }, - { id: 'opencode', name: 'OpenCode', path: '~/.config/opencode/opencode.json' }, - { id: 'cursor', name: 'Cursor', path: 'Settings → Models → OpenAI API Key' }, - { id: 'curl', name: 'cURL', path: 'Terminal' }, - { id: 'python', name: 'Python SDK', path: 'main.py' }, - { id: 'anthropic', name: 'Anthropic SDK', path: 'main.py' }, - ]; - - useEffect(() => { - loadData(); - }, []); - - useEffect(() => { - if (selectedToken) { - loadModelsForToken(selectedToken); - } - }, [selectedToken]); - - // Close model dropdown on click outside - useEffect(() => { - const handleClickOutside = (e) => { - if (modelDropdownRef.current && !modelDropdownRef.current.contains(e.target)) { - setModelDropdownOpen(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - const loadData = async () => { - setLoading(true); - try { - const res = await getTokens(); - if (res.data.success) { - const tokenList = res.data.data || []; - setTokens(tokenList); - if (tokenList.length > 0) { - setSelectedToken(tokenList[0]); - } - } - } catch (e) { /* interceptor */ } - setLoading(false); - }; - - const loadModelsForToken = async (token) => { - try { - // Try token-specific model limits first - if (token.model_limits_enabled && token.model_limits) { - const models = token.model_limits.split(',').filter(m => m.trim()); - setAvailableModels(models); - if (models.length > 0) setSelectedModel(models[0]); - return; - } - // Fall back to site-wide model list - const res = await getSiteModels(); - if (res.data.success && res.data.data) { - const list = Array.isArray(res.data.data) ? res.data.data : (res.data.data.items || []); - const modelNames = list.map(m => m.model_name || m.id || m); - setAvailableModels(modelNames); - if (modelNames.length > 0) setSelectedModel(modelNames[0]); - } - } catch (e) { /* silent */ } - }; - - const getProviderInfo = (modelName) => { - const lower = modelName.toLowerCase(); - if (lower.includes('claude')) return { provider: 'anthropic', api: 'anthropic-messages' }; - if (lower.includes('gpt') || lower.includes('o1') || lower.includes('o3')) return { provider: 'openai', api: 'openai-chat' }; - if (lower.includes('gemini')) return { provider: 'google', api: 'google-chat' }; - return { provider: 'openai', api: 'openai-chat' }; - }; - - const generateConfig = () => { - if (!selectedToken || !selectedModel) return ''; - const apiKey = 'sk-' + selectedToken.key; - - switch (selectedTool) { - case 'claudecode': - return `{ - "env": { - "ANTHROPIC_API_KEY": "${apiKey}", - "ANTHROPIC_BASE_URL": "${serverAddress}/", - "ANTHROPIC_MODEL": "${selectedModel}" - } -}`; - case 'openclaw': { - const info = getProviderInfo(selectedModel); - return `{ - "provider": "${info.provider}", - "base_url": "${serverAddress}/", - "api": "${info.api}", - "api_key": "${apiKey}", - "model": { - "id": "${selectedModel}", - "name": "${selectedModel}" - } -}`; - } - case 'opencode': - return `{ - "provider": { - "openai": { - "options": { - "baseURL": "${serverAddress}/v1", - "apiKey": "${apiKey}" - }, - "models": { - "${selectedModel}": { - "name": "${selectedModel}", - "options": { - "store": false - } - } - } - } - }, - "$schema": "https://opencode.ai/config.json" -}`; - case 'cursor': - return `API Key: ${apiKey} -Base URL: ${serverAddress}/v1 -Model: ${selectedModel}`; - case 'curl': - return `curl ${serverAddress}/v1/chat/completions \\ - -H "Content-Type: application/json" \\ - -H "Authorization: Bearer ${apiKey}" \\ - -d '{ - "model": "${selectedModel}", - "messages": [ - {"role": "user", "content": "Hello!"} - ] - }'`; - case 'python': - return `from openai import OpenAI - -client = OpenAI( - api_key="${apiKey}", - base_url="${serverAddress}/v1" -) - -response = client.chat.completions.create( - model="${selectedModel}", - messages=[ - {"role": "user", "content": "Hello!"} - ] -) - -print(response.choices[0].message.content)`; - case 'anthropic': - return `import anthropic - -client = anthropic.Anthropic( - api_key="${apiKey}", - base_url="${serverAddress}/" -) - -message = client.messages.create( - model="${selectedModel}", - max_tokens=1024, - messages=[ - {"role": "user", "content": "Hello!"} - ] -) - -print(message.content[0].text)`; - default: - return ''; - } - }; - - const getFilename = () => { - switch (selectedTool) { - case 'curl': return 'api-call.sh'; - case 'python': return 'main.py'; - case 'anthropic': return 'main.py'; - case 'cursor': return 'cursor-config.txt'; - default: return tools.find(t => t.id === selectedTool)?.path.split('/').pop() || 'config.json'; - } - }; - - const handleCopy = async () => { - const config = generateConfig(); - try { - await navigator.clipboard.writeText(config); - } catch (e) { - const ta = document.createElement('textarea'); - ta.value = config; - ta.style.position = 'fixed'; - ta.style.opacity = '0'; - document.body.appendChild(ta); - ta.select(); - document.execCommand('copy'); - document.body.removeChild(ta); - } - setCopied(true); - toast.success(t('config.copied')); - setTimeout(() => setCopied(false), 2000); - }; - - const handleDownload = () => { - const config = generateConfig(); - const filename = getFilename(); - const blob = new Blob([config], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - toast.success(t('config.downloaded')); - }; - - if (loading) { - return ( -
-
-

{t('config.loading')}

-
- ); - } - - if (tokens.length === 0) { - return ( -
- - - -

{t('config.noKeyPrompt')}

-

{t('config.noKeyDesc')}

-
- ); - } - - const config = generateConfig(); - - return ( -
- {/* Header */} -
-

- - - - {t('config.title')} -

-

{t('config.subtitle')}

-
- - {/* Controls */} -
- {/* API Key Selection */} -
- - -
- - {/* Model Selection — searchable */} - {availableModels.length > 0 && ( -
- -
- { setModelSearch(e.target.value); setModelDropdownOpen(true); }} - onFocus={() => { setModelSearch(''); setModelDropdownOpen(true); }} - className="input pr-8" - placeholder={t('config.searchModel')} - /> - - - - {modelDropdownOpen && ( -
- {availableModels - .filter(m => m.toLowerCase().includes(modelSearch.toLowerCase())) - .map((model) => ( - - )) - } - {availableModels.filter(m => m.toLowerCase().includes(modelSearch.toLowerCase())).length === 0 && ( -

{t('config.noModelMatch')}

- )} -
- )} -
-
- )} - - {/* Tool Selection */} -
- -
- {tools.map((tool) => ( - - ))} -
-
-
- - {/* Config Preview */} -
-
-
-
- - - -
- - {tools.find(t => t.id === selectedTool)?.path} - -
-
- - -
-
-
-          {config}
-        
-
-
- ); -}; - -export default ConfigExporter; diff --git a/src/index.css b/src/index.css index 04fe0bc..6fb5a15 100644 --- a/src/index.css +++ b/src/index.css @@ -121,6 +121,12 @@ @apply rounded-xl; } + /* Modal overlay — opaque card background so content is readable */ + .modal-overlay .glass, + .modal-overlay .glass-sm { + background: rgba(20, 20, 28, 0.95); + } + .btn-primary { @apply px-6 py-2.5 rounded-xl bg-brand-600 font-medium text-sm hover:bg-brand-500 active:bg-brand-700 @@ -185,6 +191,16 @@ box-shadow: 0 1px 3px rgba(0,0,0,0.06); } +.theme-light .modal-overlay .glass, +.theme-light .modal-overlay .glass-sm { + background: rgba(255, 255, 255, 0.97); +} + +.theme-claude .modal-overlay .glass, +.theme-claude .modal-overlay .glass-sm { + background: rgba(250, 246, 241, 0.97); +} + /* Light theme input focus */ .theme-light .input:focus { border-color: rgb(99 102 241); diff --git a/src/pages/Packages.jsx b/src/pages/Packages.jsx index 0d16b6a..3fb5ac7 100644 --- a/src/pages/Packages.jsx +++ b/src/pages/Packages.jsx @@ -285,7 +285,7 @@ export default function Packages() { const resetPeriod = confirmPkg.quota_reset_period || 'never'; const isSubscription = resetPeriod !== 'never'; return ( -
!subscribing && setConfirmPkg(null)}> +
!subscribing && setConfirmPkg(null)}>
e.stopPropagation()}>

{t('packages.confirmTitle')}

diff --git a/src/pages/Tokens.jsx b/src/pages/Tokens.jsx index 4d6b9d6..40ac366 100644 --- a/src/pages/Tokens.jsx +++ b/src/pages/Tokens.jsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getTokens, createToken, updateToken, deleteToken, getSiteKeyGroups } from '../api'; -import ConfigExporter from '../components/ConfigExporter'; import toast from 'react-hot-toast'; export default function Tokens() { @@ -200,7 +199,7 @@ export default function Tokens() { {/* ========== Create Modal ========== */} {showCreate && ( -

{ setShowCreate(false); setSelectedGroupId(0); }}> +
{ setShowCreate(false); setSelectedGroupId(0); }}>
e.stopPropagation()}>

{t('tokens.createApiKey')}

{selectedGroupId > 0 && (() => { @@ -240,7 +239,7 @@ export default function Tokens() { {/* ========== New Key Reveal Modal ========== */} {newKey && ( -
+

{t('tokens.newApiKey')}

@@ -270,7 +269,7 @@ export default function Tokens() { {/* ========== Delete Confirmation Modal ========== */} {deleteConfirm && ( -
setDeleteConfirm(null)}> +
setDeleteConfirm(null)}>
e.stopPropagation()}>

{t('tokens.deleteToken')}

@@ -351,11 +350,6 @@ export default function Tokens() {

)}
- - {/* Config File Generator */} -
- -
); } diff --git a/src/pages/Topup.jsx b/src/pages/Topup.jsx index 32685ad..8cf1fce 100644 --- a/src/pages/Topup.jsx +++ b/src/pages/Topup.jsx @@ -603,7 +603,7 @@ export default function Topup() { {/* Crypto Payment Modal */} {cryptoOrder && ( -
setCryptoOrder(null)}> +
setCryptoOrder(null)}>
e.stopPropagation()}>

{t('topup.cryptoPayment')}