ModelGroups.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { useEffect, useState } from 'react';
  2. import { createModelGroup, deleteModelGroup, fetchModelGroups, updateModelGroup } from '../api';
  3. import type { ModelGroup } from '../api';
  4. import './ApiKeys.css'; // 复用相同样式
  5. type EditState = { id: number; name: string; note: string };
  6. export function ModelGroups() {
  7. const [groups, setGroups] = useState<ModelGroup[]>([]);
  8. const [loading, setLoading] = useState(true);
  9. const [error, setError] = useState<string | null>(null);
  10. const [showAdd, setShowAdd] = useState(false);
  11. const [newName, setNewName] = useState('');
  12. const [newNote, setNewNote] = useState('');
  13. const [addError, setAddError] = useState<string | null>(null);
  14. const [adding, setAdding] = useState(false);
  15. const [editState, setEditState] = useState<EditState | null>(null);
  16. const [editError, setEditError] = useState<string | null>(null);
  17. const [saving, setSaving] = useState(false);
  18. const load = () => {
  19. setLoading(true);
  20. fetchModelGroups()
  21. .then(setGroups)
  22. .catch(() => setError('加载失败'))
  23. .finally(() => setLoading(false));
  24. };
  25. useEffect(() => { load(); }, []);
  26. const handleAdd = async () => {
  27. if (!newName.trim()) { setAddError('分组名称不能为空'); return; }
  28. setAdding(true); setAddError(null);
  29. try {
  30. await createModelGroup(newName.trim(), newNote.trim() || undefined);
  31. setNewName(''); setNewNote(''); setShowAdd(false);
  32. load();
  33. } catch (e) { setAddError(e instanceof Error ? e.message : String(e)); }
  34. finally { setAdding(false); }
  35. };
  36. const handleDelete = async (id: number, name: string) => {
  37. if (!confirm(`确认删除分组「${name}」?该分组下的模型将变为未分组状态。`)) return;
  38. try { await deleteModelGroup(id); if (editState?.id === id) setEditState(null); load(); }
  39. catch (e) { setError(e instanceof Error ? e.message : String(e)); }
  40. };
  41. const startEdit = (g: ModelGroup) => {
  42. if (editState?.id === g.id) { setEditState(null); return; }
  43. setEditState({ id: g.id, name: g.name, note: g.note ?? '' });
  44. setEditError(null);
  45. };
  46. const handleSave = async () => {
  47. if (!editState) return;
  48. if (!editState.name.trim()) { setEditError('分组名称不能为空'); return; }
  49. setSaving(true); setEditError(null);
  50. try {
  51. await updateModelGroup(editState.id, { name: editState.name.trim(), note: editState.note.trim() || null });
  52. setEditState(null); load();
  53. } catch (e) { setEditError(e instanceof Error ? e.message : String(e)); }
  54. finally { setSaving(false); }
  55. };
  56. return (
  57. <div className="ak-page">
  58. <div className="ak-header">
  59. <div>
  60. <div className="ak-title">模型分组管理</div>
  61. <div className="ak-subtitle">在此管理分组,然后在「模型管理」中批量归组</div>
  62. </div>
  63. <button className="ak-add-btn" onClick={() => { setShowAdd(v => !v); setAddError(null); }}>
  64. + 添加分组
  65. </button>
  66. </div>
  67. {error && <div className="ak-error">{error}</div>}
  68. {showAdd && (
  69. <div className="ak-form-card">
  70. <div className="ak-form-title">添加新分组</div>
  71. <div className="ak-form-body">
  72. <div className="ak-field">
  73. <label className="ak-label">分组名称 <span className="ak-required">*</span></label>
  74. <input className="ak-input" placeholder="如:文本生成、图像生成" value={newName}
  75. onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
  76. </div>
  77. <div className="ak-field ak-field--wide">
  78. <label className="ak-label">备注 <span className="ak-optional">可选</span></label>
  79. <input className="ak-input" placeholder="描述这个分组的用途" value={newNote}
  80. onChange={e => setNewNote(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} />
  81. </div>
  82. </div>
  83. {addError && <div className="ak-form-error">{addError}</div>}
  84. <div className="ak-form-actions">
  85. <button className="ak-btn ak-btn--confirm" onClick={handleAdd} disabled={adding}>{adding ? '添加中…' : '确认添加'}</button>
  86. <button className="ak-btn ak-btn--cancel" onClick={() => { setShowAdd(false); setAddError(null); }}>取消</button>
  87. </div>
  88. </div>
  89. )}
  90. {loading ? (
  91. <div className="ak-empty">加载中…</div>
  92. ) : groups.length === 0 ? (
  93. <div className="ak-empty">暂无分组,点击「添加分组」开始</div>
  94. ) : (
  95. <div className="ak-list">
  96. {groups.map(g => (
  97. <div key={g.id} className={`ak-card${editState?.id === g.id ? ' ak-card--editing' : ''}`}>
  98. <div className="ak-card-main">
  99. <div className="ak-card-left">
  100. <div className="ak-card-name">
  101. {g.name}
  102. <span className="mg-count-badge">{g.model_count} 个模型</span>
  103. </div>
  104. {g.note && <div className="ak-card-note">{g.note}</div>}
  105. </div>
  106. <div className="ak-card-right">
  107. <div className="ak-card-time">创建于 {new Date(g.created_at).toLocaleDateString()}</div>
  108. <div className="ak-card-actions">
  109. <button className={`ak-btn ${editState?.id === g.id ? 'ak-btn--cancel' : 'ak-btn--edit'}`} onClick={() => startEdit(g)}>
  110. {editState?.id === g.id ? '收起' : '编辑'}
  111. </button>
  112. <button className="ak-btn ak-btn--delete" onClick={() => handleDelete(g.id, g.name)}>删除</button>
  113. </div>
  114. </div>
  115. </div>
  116. {editState?.id === g.id && (
  117. <div className="ak-edit-drawer">
  118. <div className="ak-form-body">
  119. <div className="ak-field">
  120. <label className="ak-label">分组名称</label>
  121. <input className="ak-input" value={editState.name}
  122. onChange={e => setEditState(s => s ? { ...s, name: e.target.value } : s)} />
  123. </div>
  124. <div className="ak-field ak-field--wide">
  125. <label className="ak-label">备注</label>
  126. <input className="ak-input" placeholder="可选" value={editState.note}
  127. onChange={e => setEditState(s => s ? { ...s, note: e.target.value } : s)} />
  128. </div>
  129. </div>
  130. {editError && <div className="ak-form-error">{editError}</div>}
  131. <div className="ak-form-actions">
  132. <button className="ak-btn ak-btn--confirm" onClick={handleSave} disabled={saving}>{saving ? '保存中…' : '保存'}</button>
  133. <button className="ak-btn ak-btn--cancel" onClick={() => setEditState(null)}>取消</button>
  134. </div>
  135. </div>
  136. )}
  137. </div>
  138. ))}
  139. </div>
  140. )}
  141. {groups.length > 0 && <div className="ak-count">共 {groups.length} 个分组</div>}
  142. </div>
  143. );
  144. }