feat: 分站配置导出器模型搜索功能 + i18n 补全
ConfigExporter 模型下拉改为可搜索的自定义 combobox, 新增 config.searchModel / config.noModelMatch i18n 键。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
782439995f
commit
3ed0f67be0
42 files changed
+162
-119
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTokens, getSiteModels } from '../api';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -12,6 +12,9 @@ const ConfigExporter = () => {
|
||||
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;
|
||||
|
||||
@@ -35,6 +38,17 @@ const ConfigExporter = () => {
|
||||
}
|
||||
}, [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 {
|
||||
@@ -280,7 +294,7 @@ print(message.content[0].text)`;
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
{/* Model Selection — searchable */}
|
||||
{availableModels.length > 0 && (
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-page-label mb-2">
|
||||
@@ -289,15 +303,40 @@ print(message.content[0].text)`;
|
||||
</svg>
|
||||
{t('config.selectModel')}
|
||||
</label>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
className="input"
|
||||
>
|
||||
{availableModels.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative" ref={modelDropdownRef}>
|
||||
<input
|
||||
type="text"
|
||||
value={modelDropdownOpen ? modelSearch : selectedModel}
|
||||
onChange={(e) => { setModelSearch(e.target.value); setModelDropdownOpen(true); }}
|
||||
onFocus={() => { setModelSearch(''); setModelDropdownOpen(true); }}
|
||||
className="input pr-8"
|
||||
placeholder={t('config.searchModel')}
|
||||
/>
|
||||
<svg className="w-3.5 h-3.5 absolute right-3 top-1/2 -translate-y-1/2 text-page-muted pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
{modelDropdownOpen && (
|
||||
<div className="absolute z-50 mt-1 w-full max-h-60 overflow-y-auto rounded-lg border border-white/10 bg-neutral-900 shadow-xl">
|
||||
{availableModels
|
||||
.filter(m => m.toLowerCase().includes(modelSearch.toLowerCase()))
|
||||
.map((model) => (
|
||||
<button
|
||||
key={model}
|
||||
onClick={() => { setSelectedModel(model); setModelDropdownOpen(false); setModelSearch(''); }}
|
||||
className={`w-full text-left px-3 py-2 text-sm transition-colors hover:bg-white/10 ${
|
||||
model === selectedModel ? 'text-brand-400 bg-brand-500/10' : 'text-page-secondary'
|
||||
}`}
|
||||
>
|
||||
{model}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
{availableModels.filter(m => m.toLowerCase().includes(modelSearch.toLowerCase())).length === 0 && (
|
||||
<p className="px-3 py-3 text-xs text-page-muted text-center">{t('config.noModelMatch')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"config.loading": "Loading...",
|
||||
"config.loginPrompt": "Log in to generate your config",
|
||||
"config.loginDesc": "Select your API Key and model to auto-fill config files",
|
||||
"config.searchModel": "Search models...",
|
||||
"config.noModelMatch": "No models match your search",
|
||||
|
||||
"pricing.title": "Model Pricing",
|
||||
"pricing.subtitle": "Transparent, pay-as-you-go pricing for all available models. Prices shown per 1M tokens.",
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"config.loading": "加载中...",
|
||||
"config.loginPrompt": "登录后可生成您的专属配置",
|
||||
"config.loginDesc": "选择 API Key 和模型,自动填充到配置文件中",
|
||||
"config.searchModel": "搜索模型...",
|
||||
"config.noModelMatch": "没有匹配的模型",
|
||||
|
||||
"pricing.title": "模型定价",
|
||||
"pricing.subtitle": "透明的按量付费定价。价格以每百万 token 计。",
|
||||
|
||||
Reference in new issue
Block a user