GroupManager.tsx 8.4 KB

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