| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- import { useEffect, useState } from 'react';
- import { createModelGroup, deleteModelGroup, fetchModelGroups, updateModelGroup } from '../api';
- import type { ModelGroup } from '../api';
- import './ApiKeys.css'; // 复用相同样式
- type EditState = { id: number; name: string; note: string };
- export function ModelGroups() {
- 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 [newNote, setNewNote] = useState('');
- 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 load = () => {
- setLoading(true);
- fetchModelGroups()
- .then(setGroups)
- .catch(() => setError('加载失败'))
- .finally(() => setLoading(false));
- };
- useEffect(() => { load(); }, []);
- const handleAdd = async () => {
- if (!newName.trim()) { setAddError('分组名称不能为空'); return; }
- setAdding(true); setAddError(null);
- try {
- await createModelGroup(newName.trim(), newNote.trim() || undefined);
- setNewName(''); setNewNote(''); setShowAdd(false);
- load();
- } catch (e) { setAddError(e instanceof Error ? e.message : String(e)); }
- finally { setAdding(false); }
- };
- const handleDelete = async (id: number, name: string) => {
- if (!confirm(`确认删除分组「${name}」?该分组下的模型将变为未分组状态。`)) return;
- try { await deleteModelGroup(id); if (editState?.id === id) setEditState(null); load(); }
- catch (e) { setError(e instanceof Error ? e.message : String(e)); }
- };
- const startEdit = (g: ModelGroup) => {
- if (editState?.id === g.id) { setEditState(null); return; }
- setEditState({ id: g.id, name: g.name, note: g.note ?? '' });
- setEditError(null);
- };
- const handleSave = async () => {
- if (!editState) return;
- if (!editState.name.trim()) { setEditError('分组名称不能为空'); return; }
- setSaving(true); setEditError(null);
- try {
- await updateModelGroup(editState.id, { name: editState.name.trim(), note: editState.note.trim() || null });
- setEditState(null); load();
- } catch (e) { setEditError(e instanceof Error ? e.message : String(e)); }
- finally { setSaving(false); }
- };
- return (
- <div className="ak-page">
- <div className="ak-header">
- <div>
- <div className="ak-title">模型分组管理</div>
- <div className="ak-subtitle">在此管理分组,然后在「模型管理」中批量归组</div>
- </div>
- <button className="ak-add-btn" onClick={() => { setShowAdd(v => !v); setAddError(null); }}>
- + 添加分组
- </button>
- </div>
- {error && <div className="ak-error">{error}</div>}
- {showAdd && (
- <div className="ak-form-card">
- <div className="ak-form-title">添加新分组</div>
- <div className="ak-form-body">
- <div className="ak-field">
- <label className="ak-label">分组名称 <span className="ak-required">*</span></label>
- <input className="ak-input" placeholder="如:文本生成、图像生成" value={newName}
- onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
- </div>
- <div className="ak-field ak-field--wide">
- <label className="ak-label">备注 <span className="ak-optional">可选</span></label>
- <input className="ak-input" placeholder="描述这个分组的用途" value={newNote}
- onChange={e => setNewNote(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
- </div>
- </div>
- {addError && <div className="ak-form-error">{addError}</div>}
- <div className="ak-form-actions">
- <button className="ak-btn ak-btn--confirm" onClick={handleAdd} disabled={adding}>{adding ? '添加中…' : '确认添加'}</button>
- <button className="ak-btn ak-btn--cancel" onClick={() => { setShowAdd(false); setAddError(null); }}>取消</button>
- </div>
- </div>
- )}
- {loading ? (
- <div className="ak-empty">加载中…</div>
- ) : groups.length === 0 ? (
- <div className="ak-empty">暂无分组,点击「添加分组」开始</div>
- ) : (
- <div className="ak-list">
- {groups.map(g => (
- <div key={g.id} className={`ak-card${editState?.id === g.id ? ' ak-card--editing' : ''}`}>
- <div className="ak-card-main">
- <div className="ak-card-left">
- <div className="ak-card-name">
- {g.name}
- <span className="mg-count-badge">{g.model_count} 个模型</span>
- </div>
- {g.note && <div className="ak-card-note">{g.note}</div>}
- </div>
- <div className="ak-card-right">
- <div className="ak-card-time">创建于 {new Date(g.created_at).toLocaleDateString()}</div>
- <div className="ak-card-actions">
- <button className={`ak-btn ${editState?.id === g.id ? 'ak-btn--cancel' : 'ak-btn--edit'}`} onClick={() => startEdit(g)}>
- {editState?.id === g.id ? '收起' : '编辑'}
- </button>
- <button className="ak-btn ak-btn--delete" onClick={() => handleDelete(g.id, g.name)}>删除</button>
- </div>
- </div>
- </div>
- {editState?.id === g.id && (
- <div className="ak-edit-drawer">
- <div className="ak-form-body">
- <div className="ak-field">
- <label className="ak-label">分组名称</label>
- <input className="ak-input" value={editState.name}
- onChange={e => setEditState(s => s ? { ...s, name: e.target.value } : s)} />
- </div>
- <div className="ak-field ak-field--wide">
- <label className="ak-label">备注</label>
- <input className="ak-input" placeholder="可选" value={editState.note}
- onChange={e => setEditState(s => s ? { ...s, note: e.target.value } : s)} />
- </div>
- </div>
- {editError && <div className="ak-form-error">{editError}</div>}
- <div className="ak-form-actions">
- <button className="ak-btn ak-btn--confirm" onClick={handleSave} disabled={saving}>{saving ? '保存中…' : '保存'}</button>
- <button className="ak-btn ak-btn--cancel" onClick={() => setEditState(null)}>取消</button>
- </div>
- </div>
- )}
- </div>
- ))}
- </div>
- )}
- {groups.length > 0 && <div className="ak-count">共 {groups.length} 个分组</div>}
- </div>
- );
- }
|