|
|
@@ -0,0 +1,430 @@
|
|
|
+import { useEffect, useState } from 'react';
|
|
|
+import { Link } from 'react-router-dom';
|
|
|
+import {
|
|
|
+ batchAssignApiKey, batchAssignModelGroup,
|
|
|
+ createModel, deleteModel,
|
|
|
+ fetchApiKeys, fetchModelGroups, fetchModels,
|
|
|
+ updateModel,
|
|
|
+} from '../api';
|
|
|
+import type { ApiKey, Model, ModelGroup } from '../api';
|
|
|
+import './Models.css';
|
|
|
+
|
|
|
+type EditState = { id: number; name: string; url: string; api_key_id: number | null; group_id: number | null };
|
|
|
+
|
|
|
+// ── 批量配置 API Key Modal ─────────────────────────────────────────────────────
|
|
|
+function BatchKeyModal({ models, apiKeys, onClose, onDone }: {
|
|
|
+ models: Model[]; apiKeys: ApiKey[]; onClose: () => void; onDone: () => void;
|
|
|
+}) {
|
|
|
+ const [selectedKey, setSelectedKey] = useState<number | null | 'clear'>(null);
|
|
|
+ const [selectedModels, setSelectedModels] = useState<Set<number>>(new Set());
|
|
|
+ const [search, setSearch] = useState('');
|
|
|
+ const [submitting, setSubmitting] = useState(false);
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
+
|
|
|
+ const filtered = models.filter(m => !search || m.name.toLowerCase().includes(search.toLowerCase()));
|
|
|
+ const toggleModel = (id: number) => setSelectedModels(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
|
|
+ const toggleAll = () => selectedModels.size === filtered.length ? setSelectedModels(new Set()) : setSelectedModels(new Set(filtered.map(m => m.id)));
|
|
|
+
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ if (selectedKey === null) { setError('请先选择一个 API Key'); return; }
|
|
|
+ if (selectedModels.size === 0) { setError('请至少选择一个模型'); return; }
|
|
|
+ setSubmitting(true); setError(null);
|
|
|
+ try {
|
|
|
+ const r = await batchAssignApiKey(selectedKey === 'clear' ? null : selectedKey, Array.from(selectedModels));
|
|
|
+ onDone(); onClose(); alert(`已成功配置 ${r.updated} 个模型`);
|
|
|
+ } catch (e) { setError(e instanceof Error ? e.message : String(e)); }
|
|
|
+ finally { setSubmitting(false); }
|
|
|
+ };
|
|
|
+
|
|
|
+ const selectedKeyObj = typeof selectedKey === 'number' ? apiKeys.find(k => k.id === selectedKey) : null;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
|
|
|
+ <div className="modal-box">
|
|
|
+ <div className="modal-header">
|
|
|
+ <span className="modal-title">批量配置 API Key</span>
|
|
|
+ <button className="modal-close" onClick={onClose}>✕</button>
|
|
|
+ </div>
|
|
|
+ <div className="modal-section">
|
|
|
+ <div className="modal-section-label">
|
|
|
+ 选择 API Key <span className="modal-required">*</span>
|
|
|
+ <Link to="/api-keys" className="modal-manage-link" onClick={onClose}>去管理 Key →</Link>
|
|
|
+ </div>
|
|
|
+ {apiKeys.length === 0 ? (
|
|
|
+ <div className="modal-empty-hint">暂无可用 Key,请先前往 <Link to="/api-keys" onClick={onClose}>API Key 管理</Link> 添加</div>
|
|
|
+ ) : (
|
|
|
+ <div className="modal-key-list">
|
|
|
+ <label className={`modal-key-item${selectedKey === 'clear' ? ' modal-key-item--selected' : ''}`}>
|
|
|
+ <input type="radio" name="apikey" checked={selectedKey === 'clear'} onChange={() => setSelectedKey('clear')} />
|
|
|
+ <div className="modal-key-info"><span className="modal-key-name modal-key-name--clear">清除绑定</span><span className="modal-key-note">将所选模型的 API Key 设为空</span></div>
|
|
|
+ </label>
|
|
|
+ {apiKeys.map(k => (
|
|
|
+ <label key={k.id} className={`modal-key-item${selectedKey === k.id ? ' modal-key-item--selected' : ''}`}>
|
|
|
+ <input type="radio" name="apikey" checked={selectedKey === k.id} onChange={() => setSelectedKey(k.id)} />
|
|
|
+ <div className="modal-key-info"><span className="modal-key-name">{k.name}</span>{k.note && <span className="modal-key-note">{k.note}</span>}</div>
|
|
|
+ </label>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <div className="modal-section">
|
|
|
+ <div className="modal-section-label">
|
|
|
+ 选择模型
|
|
|
+ <span className="modal-count-badge">{selectedModels.size} / {models.length}</span>
|
|
|
+ <button className="modal-select-all" onClick={toggleAll}>{selectedModels.size === filtered.length && filtered.length > 0 ? '取消全选' : '全选'}</button>
|
|
|
+ </div>
|
|
|
+ <input className="modal-search" placeholder="搜索模型名称…" value={search} onChange={e => setSearch(e.target.value)} />
|
|
|
+ <div className="modal-model-list">
|
|
|
+ {filtered.map(m => (
|
|
|
+ <label key={m.id} className={`modal-model-item${selectedModels.has(m.id) ? ' modal-model-item--selected' : ''}`}>
|
|
|
+ <input type="checkbox" checked={selectedModels.has(m.id)} onChange={() => toggleModel(m.id)} />
|
|
|
+ <span className="modal-model-name">{m.name}</span>
|
|
|
+ {m.api_key_name ? <span className="modal-model-has-key">{m.api_key_name}</span> : <span className="modal-model-no-key">未绑定</span>}
|
|
|
+ </label>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {selectedKey !== null && selectedModels.size > 0 && (
|
|
|
+ <div className="modal-preview">
|
|
|
+ 将 <strong>{selectedModels.size}</strong> 个模型绑定到
|
|
|
+ {selectedKey === 'clear' ? <span className="modal-preview-clear">「清除绑定」</span> : <span className="modal-preview-key">「{selectedKeyObj?.name}」</span>}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {error && <div className="modal-error">{error}</div>}
|
|
|
+ <div className="modal-footer">
|
|
|
+ <button className="modal-btn modal-btn--cancel" onClick={onClose}>关闭</button>
|
|
|
+ <button className="modal-btn modal-btn--confirm" onClick={handleSubmit} disabled={submitting}>
|
|
|
+ {submitting ? '配置中…' : `确认配置(${selectedModels.size} 个模型)`}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// ── 批量分组 Modal ─────────────────────────────────────────────────────────────
|
|
|
+function BatchGroupModal({ models, groups, onClose, onDone }: {
|
|
|
+ models: Model[]; groups: ModelGroup[]; onClose: () => void; onDone: () => void;
|
|
|
+}) {
|
|
|
+ const [selectedGroup, setSelectedGroup] = useState<number | null | 'clear'>(null);
|
|
|
+ const [selectedModels, setSelectedModels] = useState<Set<number>>(new Set());
|
|
|
+ const [search, setSearch] = useState('');
|
|
|
+ const [submitting, setSubmitting] = useState(false);
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
+
|
|
|
+ const filtered = models.filter(m => !search || m.name.toLowerCase().includes(search.toLowerCase()));
|
|
|
+ const toggleModel = (id: number) => setSelectedModels(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
|
|
+ const toggleAll = () => selectedModels.size === filtered.length ? setSelectedModels(new Set()) : setSelectedModels(new Set(filtered.map(m => m.id)));
|
|
|
+
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ if (selectedGroup === null) { setError('请先选择一个分组'); return; }
|
|
|
+ if (selectedModels.size === 0) { setError('请至少选择一个模型'); return; }
|
|
|
+ setSubmitting(true); setError(null);
|
|
|
+ try {
|
|
|
+ const r = await batchAssignModelGroup(selectedGroup === 'clear' ? null : selectedGroup, Array.from(selectedModels));
|
|
|
+ onDone(); onClose(); alert(`已成功分组 ${r.updated} 个模型`);
|
|
|
+ } catch (e) { setError(e instanceof Error ? e.message : String(e)); }
|
|
|
+ finally { setSubmitting(false); }
|
|
|
+ };
|
|
|
+
|
|
|
+ const selectedGroupObj = typeof selectedGroup === 'number' ? groups.find(g => g.id === selectedGroup) : null;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
|
|
|
+ <div className="modal-box">
|
|
|
+ <div className="modal-header">
|
|
|
+ <span className="modal-title">批量设置分组</span>
|
|
|
+ <button className="modal-close" onClick={onClose}>✕</button>
|
|
|
+ </div>
|
|
|
+ <div className="modal-section">
|
|
|
+ <div className="modal-section-label">
|
|
|
+ 选择分组 <span className="modal-required">*</span>
|
|
|
+ <Link to="/model-groups" className="modal-manage-link" onClick={onClose}>去管理分组 →</Link>
|
|
|
+ </div>
|
|
|
+ {groups.length === 0 ? (
|
|
|
+ <div className="modal-empty-hint">暂无分组,请先前往 <Link to="/model-groups" onClick={onClose}>分组管理</Link> 添加</div>
|
|
|
+ ) : (
|
|
|
+ <div className="modal-key-list">
|
|
|
+ <label className={`modal-key-item${selectedGroup === 'clear' ? ' modal-key-item--selected' : ''}`}>
|
|
|
+ <input type="radio" name="group" checked={selectedGroup === 'clear'} onChange={() => setSelectedGroup('clear')} />
|
|
|
+ <div className="modal-key-info"><span className="modal-key-name modal-key-name--clear">清除分组</span><span className="modal-key-note">将所选模型设为未分组</span></div>
|
|
|
+ </label>
|
|
|
+ {groups.map(g => (
|
|
|
+ <label key={g.id} className={`modal-key-item${selectedGroup === g.id ? ' modal-key-item--selected' : ''}`}>
|
|
|
+ <input type="radio" name="group" checked={selectedGroup === g.id} onChange={() => setSelectedGroup(g.id)} />
|
|
|
+ <div className="modal-key-info">
|
|
|
+ <span className="modal-key-name">{g.name}</span>
|
|
|
+ {g.note && <span className="modal-key-note">{g.note}</span>}
|
|
|
+ </div>
|
|
|
+ </label>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <div className="modal-section">
|
|
|
+ <div className="modal-section-label">
|
|
|
+ 选择模型
|
|
|
+ <span className="modal-count-badge">{selectedModels.size} / {models.length}</span>
|
|
|
+ <button className="modal-select-all" onClick={toggleAll}>{selectedModels.size === filtered.length && filtered.length > 0 ? '取消全选' : '全选'}</button>
|
|
|
+ </div>
|
|
|
+ <input className="modal-search" placeholder="搜索模型名称…" value={search} onChange={e => setSearch(e.target.value)} />
|
|
|
+ <div className="modal-model-list">
|
|
|
+ {filtered.map(m => (
|
|
|
+ <label key={m.id} className={`modal-model-item${selectedModels.has(m.id) ? ' modal-model-item--selected' : ''}`}>
|
|
|
+ <input type="checkbox" checked={selectedModels.has(m.id)} onChange={() => toggleModel(m.id)} />
|
|
|
+ <span className="modal-model-name">{m.name}</span>
|
|
|
+ {m.group_name ? <span className="modal-model-has-key">{m.group_name}</span> : <span className="modal-model-no-key">未分组</span>}
|
|
|
+ </label>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {selectedGroup !== null && selectedModels.size > 0 && (
|
|
|
+ <div className="modal-preview">
|
|
|
+ 将 <strong>{selectedModels.size}</strong> 个模型移入
|
|
|
+ {selectedGroup === 'clear' ? <span className="modal-preview-clear">「清除分组」</span> : <span className="modal-preview-key">「{selectedGroupObj?.name}」</span>}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {error && <div className="modal-error">{error}</div>}
|
|
|
+ <div className="modal-footer">
|
|
|
+ <button className="modal-btn modal-btn--cancel" onClick={onClose}>关闭</button>
|
|
|
+ <button className="modal-btn modal-btn--confirm" onClick={handleSubmit} disabled={submitting}>
|
|
|
+ {submitting ? '设置中…' : `确认设置(${selectedModels.size} 个模型)`}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// ── 主页面 ────────────────────────────────────────────────────────────────────
|
|
|
+export function Models() {
|
|
|
+ const [models, setModels] = useState<Model[]>([]);
|
|
|
+ const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
|
|
+ const [groups, setGroups] = useState<ModelGroup[]>([]);
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
+
|
|
|
+ const [showAdd, setShowAdd] = useState(false);
|
|
|
+ const [newName, setNewName] = useState('');
|
|
|
+ const [newUrl, setNewUrl] = useState('');
|
|
|
+ const [newApiKeyId, setNewApiKeyId] = useState<number | ''>('');
|
|
|
+ const [newGroupId, setNewGroupId] = useState<number | ''>('');
|
|
|
+ const [addError, setAddError] = useState<string | null>(null);
|
|
|
+ const [adding, setAdding] = useState(false);
|
|
|
+
|
|
|
+ const [editState, setEditState] = useState<EditState | null>(null);
|
|
|
+ const [editError, setEditError] = useState<string | null>(null);
|
|
|
+ const [saving, setSaving] = useState(false);
|
|
|
+
|
|
|
+ const [showBatchKey, setShowBatchKey] = useState(false);
|
|
|
+ const [showBatchGroup, setShowBatchGroup] = useState(false);
|
|
|
+
|
|
|
+ const load = () => {
|
|
|
+ setLoading(true);
|
|
|
+ Promise.all([fetchModels(), fetchApiKeys(), fetchModelGroups()])
|
|
|
+ .then(([m, k, g]) => { setModels(m); setApiKeys(k); setGroups(g); })
|
|
|
+ .catch(() => setError('加载失败'))
|
|
|
+ .finally(() => setLoading(false));
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => { load(); }, []);
|
|
|
+
|
|
|
+ const handleAdd = async () => {
|
|
|
+ if (!newName.trim() || !newUrl.trim()) { setAddError('名称和 URL 不能为空'); return; }
|
|
|
+ setAdding(true); setAddError(null);
|
|
|
+ try {
|
|
|
+ await createModel(newName.trim(), newUrl.trim(),
|
|
|
+ newApiKeyId !== '' ? newApiKeyId : undefined,
|
|
|
+ newGroupId !== '' ? newGroupId : undefined,
|
|
|
+ );
|
|
|
+ setNewName(''); setNewUrl(''); setNewApiKeyId(''); setNewGroupId(''); setShowAdd(false);
|
|
|
+ load();
|
|
|
+ } catch (e) { setAddError(e instanceof Error ? e.message : String(e)); }
|
|
|
+ finally { setAdding(false); }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
+ if (!confirm('确认删除该模型?')) return;
|
|
|
+ try { await deleteModel(id); if (editState?.id === id) setEditState(null); load(); }
|
|
|
+ catch (e) { setError(e instanceof Error ? e.message : String(e)); }
|
|
|
+ };
|
|
|
+
|
|
|
+ const startEdit = (m: Model) => {
|
|
|
+ if (editState?.id === m.id) { setEditState(null); return; }
|
|
|
+ setEditState({ id: m.id, name: m.name, url: m.url, api_key_id: m.api_key_id, group_id: m.group_id });
|
|
|
+ setEditError(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSave = async () => {
|
|
|
+ if (!editState) return;
|
|
|
+ if (!editState.name.trim() || !editState.url.trim()) { setEditError('名称和 URL 不能为空'); return; }
|
|
|
+ setSaving(true); setEditError(null);
|
|
|
+ try {
|
|
|
+ await updateModel(editState.id, {
|
|
|
+ name: editState.name.trim(), url: editState.url.trim(),
|
|
|
+ api_key_id: editState.api_key_id, group_id: editState.group_id,
|
|
|
+ });
|
|
|
+ setEditState(null); load();
|
|
|
+ } catch (e) { setEditError(e instanceof Error ? e.message : String(e)); }
|
|
|
+ finally { setSaving(false); }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="models-page">
|
|
|
+ <div className="models-header">
|
|
|
+ <span className="models-title">模型管理</span>
|
|
|
+ <div className="models-header-actions">
|
|
|
+ <button className="models-batch-btn models-batch-btn--group" onClick={() => setShowBatchGroup(true)}>
|
|
|
+ ▤ 批量设置分组
|
|
|
+ </button>
|
|
|
+ <button className="models-batch-btn" onClick={() => setShowBatchKey(true)}>
|
|
|
+ 🔑 批量配置 API Key
|
|
|
+ </button>
|
|
|
+ <button className="models-add-btn" onClick={() => { setShowAdd(v => !v); setAddError(null); }}>
|
|
|
+ + 添加模型
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {error && <div className="models-error">{error}</div>}
|
|
|
+
|
|
|
+ {showAdd && (
|
|
|
+ <div className="models-form-card">
|
|
|
+ <div className="models-form-row">
|
|
|
+ <div className="models-form-field">
|
|
|
+ <label className="models-form-label">模型名称</label>
|
|
|
+ <input className="models-input" placeholder="如 qwen3-max" value={newName}
|
|
|
+ onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
|
|
|
+ </div>
|
|
|
+ <div className="models-form-field models-form-field--wide">
|
|
|
+ <label className="models-form-label">URL</label>
|
|
|
+ <input className="models-input" placeholder="https://bailian.console.aliyun.com/..." value={newUrl}
|
|
|
+ onChange={e => setNewUrl(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
|
|
|
+ </div>
|
|
|
+ <div className="models-form-field">
|
|
|
+ <label className="models-form-label">分组 <span className="models-optional">可选</span></label>
|
|
|
+ <select className="models-input models-select" value={newGroupId}
|
|
|
+ onChange={e => setNewGroupId(e.target.value === '' ? '' : Number(e.target.value))}>
|
|
|
+ <option value="">— 不分组 —</option>
|
|
|
+ {groups.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div className="models-form-field">
|
|
|
+ <label className="models-form-label">API Key <span className="models-optional">可选</span></label>
|
|
|
+ <select className="models-input models-select" value={newApiKeyId}
|
|
|
+ onChange={e => setNewApiKeyId(e.target.value === '' ? '' : Number(e.target.value))}>
|
|
|
+ <option value="">— 不绑定 —</option>
|
|
|
+ {apiKeys.map(k => <option key={k.id} value={k.id}>{k.name}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {addError && <div className="models-form-error">{addError}</div>}
|
|
|
+ <div className="models-form-actions">
|
|
|
+ <button className="models-btn models-btn--confirm" onClick={handleAdd} disabled={adding}>{adding ? '添加中…' : '确认添加'}</button>
|
|
|
+ <button className="models-btn models-btn--cancel" onClick={() => { setShowAdd(false); setAddError(null); }}>取消</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {loading ? (
|
|
|
+ <div className="models-loading">加载中…</div>
|
|
|
+ ) : models.length === 0 ? (
|
|
|
+ <div className="models-empty">暂无模型,点击「添加模型」开始</div>
|
|
|
+ ) : (
|
|
|
+ <div className="models-table-wrap">
|
|
|
+ <table className="models-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th style={{ width: 150 }}>名称</th>
|
|
|
+ <th>URL</th>
|
|
|
+ <th style={{ width: 120 }}>分组</th>
|
|
|
+ <th style={{ width: 130 }}>API Key</th>
|
|
|
+ <th style={{ width: 150 }}>创建时间</th>
|
|
|
+ <th style={{ width: 120 }}>操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ {models.map(m => (
|
|
|
+ <>
|
|
|
+ <tr key={m.id} className={`models-row${editState?.id === m.id ? ' models-row--active' : ''}`}>
|
|
|
+ <td className="models-td-name">{m.name}</td>
|
|
|
+ <td className="models-td-url">
|
|
|
+ <a href={m.url} target="_blank" rel="noopener noreferrer" className="models-url-link" title={m.url}>{m.url}</a>
|
|
|
+ </td>
|
|
|
+ <td className="models-td-key">
|
|
|
+ {m.group_name
|
|
|
+ ? <span className="models-group-badge">{m.group_name}</span>
|
|
|
+ : <span className="models-key-empty">—</span>}
|
|
|
+ </td>
|
|
|
+ <td className="models-td-key">
|
|
|
+ {m.api_key_name
|
|
|
+ ? <span className="models-key-badge">{m.api_key_name}</span>
|
|
|
+ : <span className="models-key-empty">—</span>}
|
|
|
+ </td>
|
|
|
+ <td className="models-td-time">{new Date(m.created_at).toLocaleString()}</td>
|
|
|
+ <td>
|
|
|
+ <div className="models-actions">
|
|
|
+ <button className={`models-btn ${editState?.id === m.id ? 'models-btn--cancel' : 'models-btn--edit'}`} onClick={() => startEdit(m)}>
|
|
|
+ {editState?.id === m.id ? '收起' : '编辑'}
|
|
|
+ </button>
|
|
|
+ <button className="models-btn models-btn--delete" onClick={() => handleDelete(m.id)}>删除</button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ {editState?.id === m.id && (
|
|
|
+ <tr key={`edit-${m.id}`} className="models-row-edit">
|
|
|
+ <td colSpan={6}>
|
|
|
+ <div className="models-edit-drawer">
|
|
|
+ <div className="models-edit-fields">
|
|
|
+ <div className="models-edit-field">
|
|
|
+ <label className="models-form-label">名称</label>
|
|
|
+ <input className="models-input" value={editState.name}
|
|
|
+ onChange={e => setEditState(s => s ? { ...s, name: e.target.value } : s)} />
|
|
|
+ </div>
|
|
|
+ <div className="models-edit-field models-edit-field--wide">
|
|
|
+ <label className="models-form-label">URL</label>
|
|
|
+ <input className="models-input" value={editState.url}
|
|
|
+ onChange={e => setEditState(s => s ? { ...s, url: e.target.value } : s)} />
|
|
|
+ </div>
|
|
|
+ <div className="models-edit-field">
|
|
|
+ <label className="models-form-label">分组</label>
|
|
|
+ <select className="models-input models-select"
|
|
|
+ value={editState.group_id ?? ''}
|
|
|
+ onChange={e => setEditState(s => s ? { ...s, group_id: e.target.value === '' ? null : Number(e.target.value) } : s)}>
|
|
|
+ <option value="">— 不分组 —</option>
|
|
|
+ {groups.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div className="models-edit-field">
|
|
|
+ <label className="models-form-label">API Key</label>
|
|
|
+ <select className="models-input models-select"
|
|
|
+ value={editState.api_key_id ?? ''}
|
|
|
+ onChange={e => setEditState(s => s ? { ...s, api_key_id: e.target.value === '' ? null : Number(e.target.value) } : s)}>
|
|
|
+ <option value="">— 不绑定 —</option>
|
|
|
+ {apiKeys.map(k => <option key={k.id} value={k.id}>{k.name}</option>)}
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {editError && <div className="models-form-error">{editError}</div>}
|
|
|
+ <div className="models-form-actions">
|
|
|
+ <button className="models-btn models-btn--save" onClick={handleSave} disabled={saving}>{saving ? '保存中…' : '保存'}</button>
|
|
|
+ <button className="models-btn models-btn--cancel" onClick={() => setEditState(null)}>取消</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ ))}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {models.length > 0 && <div className="models-count">共 {models.length} 个模型</div>}
|
|
|
+
|
|
|
+ {showBatchKey && <BatchKeyModal models={models} apiKeys={apiKeys} onClose={() => setShowBatchKey(false)} onDone={load} />}
|
|
|
+ {showBatchGroup && <BatchGroupModal models={models} groups={groups} onClose={() => setShowBatchGroup(false)} onDone={load} />}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|