GroupManager.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import React, { useState, useEffect } from 'react';
  2. import { KnowledgeGroup, CreateGroupData, UpdateGroupData } from '../types';
  3. import { knowledgeGroupService } from '../services/knowledgeGroupService';
  4. import { useToast } from '../contexts/ToastContext';
  5. import { Folder, Plus, Edit2, Trash2, X } from 'lucide-react';
  6. interface GroupManagerProps {
  7. groups: KnowledgeGroup[];
  8. onGroupsChange: (groups: KnowledgeGroup[]) => void;
  9. }
  10. const DEFAULT_COLORS = [
  11. '#3B82F6', '#10B981', '#F59E0B', '#EF4444',
  12. '#8B5CF6', '#06B6D4', '#84CC16', '#F97316'
  13. ];
  14. export const GroupManager: React.FC<GroupManagerProps> = ({ groups, onGroupsChange }) => {
  15. const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
  16. const [editingGroup, setEditingGroup] = useState<KnowledgeGroup | null>(null);
  17. const [formData, setFormData] = useState<CreateGroupData>({
  18. name: '',
  19. description: '',
  20. color: DEFAULT_COLORS[0],
  21. });
  22. const [loading, setLoading] = useState(false);
  23. const { showToast } = useToast();
  24. const resetForm = () => {
  25. setFormData({
  26. name: '',
  27. description: '',
  28. color: DEFAULT_COLORS[0],
  29. });
  30. };
  31. const handleCreate = async (e: React.FormEvent) => {
  32. e.preventDefault();
  33. if (!formData.name.trim()) return;
  34. setLoading(true);
  35. try {
  36. const newGroup = await knowledgeGroupService.createGroup(formData);
  37. onGroupsChange([...groups, newGroup]);
  38. setIsCreateModalOpen(false);
  39. resetForm();
  40. showToast('分组创建成功', 'success');
  41. } catch (error) {
  42. showToast('创建分组失败', 'error');
  43. } finally {
  44. setLoading(false);
  45. }
  46. };
  47. const handleUpdate = async (e: React.FormEvent) => {
  48. e.preventDefault();
  49. if (!editingGroup || !formData.name.trim()) return;
  50. setLoading(true);
  51. try {
  52. const updatedGroup = await knowledgeGroupService.updateGroup(editingGroup.id, formData);
  53. onGroupsChange(groups.map(g => g.id === editingGroup.id ? updatedGroup : g));
  54. setEditingGroup(null);
  55. resetForm();
  56. showToast('分组更新成功', 'success');
  57. } catch (error) {
  58. showToast('更新分组失败', 'error');
  59. } finally {
  60. setLoading(false);
  61. }
  62. };
  63. const handleDelete = async (group: KnowledgeGroup) => {
  64. if (!confirm(`确定要删除分组"${group.name}"吗?`)) return;
  65. try {
  66. await knowledgeGroupService.deleteGroup(group.id);
  67. onGroupsChange(groups.filter(g => g.id !== group.id));
  68. showToast('分组删除成功', 'success');
  69. } catch (error) {
  70. showToast('删除分组失败', 'error');
  71. }
  72. };
  73. const openEditModal = (group: KnowledgeGroup) => {
  74. setEditingGroup(group);
  75. setFormData({
  76. name: group.name,
  77. description: group.description || '',
  78. color: group.color,
  79. });
  80. };
  81. const closeModal = () => {
  82. setIsCreateModalOpen(false);
  83. setEditingGroup(null);
  84. resetForm();
  85. };
  86. const isModalOpen = isCreateModalOpen || editingGroup !== null;
  87. return (
  88. <div className="space-y-4">
  89. {/* 分组列表 */}
  90. <div className="space-y-2">
  91. {groups.map((group) => (
  92. <div
  93. key={group.id}
  94. className="flex items-center justify-between p-3 bg-white rounded-lg border hover:shadow-sm transition-shadow"
  95. >
  96. <div className="flex items-center space-x-3">
  97. <div
  98. className="w-4 h-4 rounded-full"
  99. style={{ backgroundColor: group.color }}
  100. />
  101. <div>
  102. <div className="font-medium text-gray-900">{group.name}</div>
  103. {group.description && (
  104. <div className="text-sm text-gray-500">{group.description}</div>
  105. )}
  106. <div className="text-xs text-gray-400">
  107. {group.fileCount} 个文件
  108. </div>
  109. </div>
  110. </div>
  111. <div className="flex items-center space-x-2">
  112. <button
  113. onClick={() => openEditModal(group)}
  114. className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
  115. >
  116. <Edit2 size={16} />
  117. </button>
  118. <button
  119. onClick={() => handleDelete(group)}
  120. className="p-1 text-gray-400 hover:text-red-600 transition-colors"
  121. >
  122. <Trash2 size={16} />
  123. </button>
  124. </div>
  125. </div>
  126. ))}
  127. </div>
  128. {/* 创建按钮 */}
  129. <button
  130. onClick={() => setIsCreateModalOpen(true)}
  131. className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-600 transition-colors"
  132. title="创建新分组"
  133. >
  134. <Plus size={18} />
  135. </button>
  136. {/* 创建/编辑模态框 */}
  137. {isModalOpen && (
  138. <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
  139. <div className="bg-white rounded-lg p-6 w-full max-w-md">
  140. <div className="flex items-center justify-between mb-4">
  141. <h3 className="text-lg font-semibold">
  142. {editingGroup ? '编辑分组' : '创建分组'}
  143. </h3>
  144. <button
  145. onClick={closeModal}
  146. className="text-gray-400 hover:text-gray-600"
  147. >
  148. <X size={20} />
  149. </button>
  150. </div>
  151. <form onSubmit={editingGroup ? handleUpdate : handleCreate} className="space-y-4">
  152. <div>
  153. <label className="block text-sm font-medium text-gray-700 mb-1">
  154. 分组名称 *
  155. </label>
  156. <input
  157. type="text"
  158. value={formData.name}
  159. onChange={(e) => setFormData({ ...formData, name: e.target.value })}
  160. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
  161. placeholder="输入分组名称"
  162. required
  163. />
  164. </div>
  165. <div>
  166. <label className="block text-sm font-medium text-gray-700 mb-1">
  167. 描述
  168. </label>
  169. <textarea
  170. value={formData.description}
  171. onChange={(e) => setFormData({ ...formData, description: e.target.value })}
  172. className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
  173. placeholder="输入分组描述(可选)"
  174. rows={3}
  175. />
  176. </div>
  177. <div>
  178. <label className="block text-sm font-medium text-gray-700 mb-2">
  179. 颜色标识
  180. </label>
  181. <div className="flex space-x-2">
  182. {DEFAULT_COLORS.map((color) => (
  183. <button
  184. key={color}
  185. type="button"
  186. onClick={() => setFormData({ ...formData, color })}
  187. className={`w-8 h-8 rounded-full border-2 ${
  188. formData.color === color ? 'border-gray-400' : 'border-gray-200'
  189. }`}
  190. style={{ backgroundColor: color }}
  191. />
  192. ))}
  193. </div>
  194. </div>
  195. <div className="flex space-x-3 pt-4">
  196. <button
  197. type="button"
  198. onClick={closeModal}
  199. className="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
  200. >
  201. 取消
  202. </button>
  203. <button
  204. type="submit"
  205. disabled={loading || !formData.name.trim()}
  206. className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
  207. >
  208. {loading ? '保存中...' : (editingGroup ? '更新' : '创建')}
  209. </button>
  210. </div>
  211. </form>
  212. </div>
  213. </div>
  214. )}
  215. </div>
  216. );
  217. };