|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
import { Link } from 'react-router-dom';
|
|
|
import * as XLSX from 'xlsx';
|
|
|
import {
|
|
|
- batchAssignApiKey, batchAssignModelGroup, batchDeleteModels,
|
|
|
+ batchAssignModelGroup, batchDeleteModels,
|
|
|
createModel, deleteModel,
|
|
|
fetchApiKeys, fetchModelGroups, fetchModels,
|
|
|
updateModel, upsertModel,
|
|
|
@@ -12,97 +12,6 @@ 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;
|
|
|
@@ -200,8 +109,8 @@ function BatchGroupModal({ models, groups, onClose, onDone }: {
|
|
|
// ── 主页面 ────────────────────────────────────────────────────────────────────
|
|
|
export function Models() {
|
|
|
const [models, setModels] = useState<Model[]>([]);
|
|
|
- const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
|
|
const [groups, setGroups] = useState<ModelGroup[]>([]);
|
|
|
+ const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
@@ -217,7 +126,6 @@ export function Models() {
|
|
|
const [editError, setEditError] = useState<string | null>(null);
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
- const [showBatchKey, setShowBatchKey] = useState(false);
|
|
|
const [showBatchGroup, setShowBatchGroup] = useState(false);
|
|
|
|
|
|
// 多选(用于批量删除)
|
|
|
@@ -233,8 +141,8 @@ export function Models() {
|
|
|
|
|
|
const load = () => {
|
|
|
setLoading(true);
|
|
|
- Promise.all([fetchModels(), fetchApiKeys(), fetchModelGroups()])
|
|
|
- .then(([m, k, g]) => { setModels(m); setApiKeys(k); setGroups(g); })
|
|
|
+ Promise.all([fetchModels(), fetchModelGroups(), fetchApiKeys()])
|
|
|
+ .then(([m, g, k]) => { setModels(m); setGroups(g); setApiKeys(k); })
|
|
|
.catch(() => setError('加载失败'))
|
|
|
.finally(() => setLoading(false));
|
|
|
};
|
|
|
@@ -376,7 +284,8 @@ export function Models() {
|
|
|
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,
|
|
|
+ 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)); }
|
|
|
@@ -391,9 +300,6 @@ export function Models() {
|
|
|
<button className="models-batch-btn models-batch-btn--group" onClick={() => setShowBatchGroup(true)}>
|
|
|
▤ 批量设置分组
|
|
|
</button>
|
|
|
- <button className="models-batch-btn" onClick={() => setShowBatchKey(true)}>
|
|
|
- 🔑 批量配置 API Key
|
|
|
- </button>
|
|
|
{selectedIds.size > 0 && (
|
|
|
<button className="models-batch-btn models-batch-btn--delete" onClick={handleBatchDelete} disabled={batchDeleting}>
|
|
|
🗑 删除选中 ({selectedIds.size})
|
|
|
@@ -485,14 +391,6 @@ export function Models() {
|
|
|
<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}
|
|
|
@@ -501,6 +399,14 @@ export function Models() {
|
|
|
{apiKeys.map(k => <option key={k.id} value={k.id}>{k.name}</option>)}
|
|
|
</select>
|
|
|
</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>
|
|
|
{addError && <div className="models-form-error">{addError}</div>}
|
|
|
<div className="models-form-actions">
|
|
|
@@ -529,8 +435,7 @@ export function Models() {
|
|
|
</th>
|
|
|
<th style={{ width: 150 }}>名称</th>
|
|
|
<th>URL</th>
|
|
|
- <th style={{ width: 120 }}>分组</th>
|
|
|
- <th style={{ width: 130 }}>API Key</th>
|
|
|
+ <th style={{ width: 180 }}>分组 / API Key</th>
|
|
|
<th style={{ width: 150 }}>创建时间</th>
|
|
|
<th style={{ width: 120 }}>操作</th>
|
|
|
</tr>
|
|
|
@@ -549,12 +454,9 @@ export function Models() {
|
|
|
<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>}
|
|
|
+ : 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>
|
|
|
@@ -582,16 +484,7 @@ export function Models() {
|
|
|
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>
|
|
|
+ <label className="models-form-label">API Key <span className="models-optional">可选</span></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)}>
|
|
|
@@ -599,6 +492,15 @@ export function Models() {
|
|
|
{apiKeys.map(k => <option key={k.id} value={k.id}>{k.name}</option>)}
|
|
|
</select>
|
|
|
</div>
|
|
|
+ <div className="models-edit-field">
|
|
|
+ <label className="models-form-label">分组 <span className="models-optional">可选</span></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>
|
|
|
{editError && <div className="models-form-error">{editError}</div>}
|
|
|
<div className="models-form-actions">
|
|
|
@@ -618,7 +520,6 @@ export function Models() {
|
|
|
|
|
|
{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>
|
|
|
);
|