GroupSelectionDrawer.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import React, { useState } from 'react';
  2. import { createPortal } from 'react-dom';
  3. import { useLanguage } from '../contexts/LanguageContext';
  4. import { KnowledgeGroup } from '../types';
  5. import { Check, X, Search, Database } from 'lucide-react';
  6. interface GroupSelectionDrawerProps {
  7. isOpen: boolean;
  8. onClose: () => void;
  9. groups: KnowledgeGroup[];
  10. selectedGroups: string[];
  11. onSelectionChange: (groupIds: string[]) => void;
  12. }
  13. export const GroupSelectionDrawer: React.FC<GroupSelectionDrawerProps> = ({
  14. isOpen,
  15. onClose,
  16. groups,
  17. selectedGroups,
  18. onSelectionChange
  19. }) => {
  20. const { t } = useLanguage();
  21. const [searchTerm, setSearchTerm] = useState('');
  22. if (!isOpen) return null;
  23. const filteredGroups = groups.filter(g =>
  24. g.name.toLowerCase().includes(searchTerm.toLowerCase())
  25. );
  26. const isAllSelected = selectedGroups.length === 0;
  27. const handleToggleGroup = (groupId: string) => {
  28. if (selectedGroups.includes(groupId)) {
  29. onSelectionChange(selectedGroups.filter(id => id !== groupId));
  30. } else {
  31. onSelectionChange([...selectedGroups, groupId]);
  32. }
  33. };
  34. const handleSelectAll = () => {
  35. onSelectionChange([]);
  36. };
  37. return createPortal(
  38. <div className="fixed inset-0 z-50 overflow-hidden">
  39. <div className="absolute inset-0 bg-black/30 backdrop-blur-sm transition-opacity" onClick={onClose} />
  40. <div className="absolute inset-y-0 right-0 max-w-md w-full flex">
  41. <div className="flex-1 flex flex-col bg-white shadow-xl animate-in slide-in-from-right duration-300">
  42. <div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
  43. <h2 className="text-lg font-medium text-gray-900 flex items-center gap-2">
  44. <Database size={20} />
  45. {t('selectKnowledgeGroups')}
  46. </h2>
  47. <button
  48. onClick={onClose}
  49. className="text-gray-400 hover:text-gray-500 focus:outline-none"
  50. >
  51. <X size={24} />
  52. </button>
  53. </div>
  54. {/* Search Box */}
  55. <div className="p-4 border-b border-gray-100">
  56. <div className="relative">
  57. <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
  58. <input
  59. type="text"
  60. placeholder={t('searchGroupsPlaceholder')}
  61. value={searchTerm}
  62. onChange={(e) => setSearchTerm(e.target.value)}
  63. className="w-full pl-9 pr-4 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-gray-50"
  64. />
  65. </div>
  66. </div>
  67. <div className="flex-1 overflow-y-auto p-4 space-y-1">
  68. {!searchTerm && (
  69. <div
  70. onClick={handleSelectAll}
  71. className={`flex items-center px-4 py-3.5 cursor-pointer hover:bg-slate-50 rounded-xl transition-all ${isAllSelected ? 'bg-blue-50/50 text-blue-700 outline outline-1 outline-blue-200' : 'text-slate-700 border border-transparent'
  72. }`}
  73. >
  74. <div className={`w-5 h-5 mr-3 border rounded-md flex items-center justify-center transition-colors ${isAllSelected ? 'bg-blue-600 border-blue-600' : 'border-slate-300'
  75. }`}>
  76. {isAllSelected && <Check size={14} className="text-white" />}
  77. </div>
  78. <span className="font-semibold text-sm">{t('all')}</span>
  79. </div>
  80. )}
  81. {filteredGroups.map((group) => {
  82. const isSelected = selectedGroups.includes(group.id);
  83. return (
  84. <div
  85. key={group.id}
  86. onClick={() => handleToggleGroup(group.id)}
  87. className={`flex items-center px-4 py-3 cursor-pointer hover:bg-slate-50 rounded-xl transition-all ${isSelected ? 'bg-blue-50/50 outline outline-1 outline-blue-200' : 'border border-transparent'
  88. }`}
  89. >
  90. <div className={`w-5 h-5 mr-3 border rounded-md flex items-center justify-center flex-shrink-0 transition-colors ${isSelected ? 'bg-blue-600 border-blue-600' : 'border-slate-300'
  91. }`}>
  92. {isSelected && <Check size={14} className="text-white" />}
  93. </div>
  94. <div
  95. className="w-3 h-3 rounded-full mr-3 flex-shrink-0 shadow-sm"
  96. style={{ backgroundColor: group.color }}
  97. />
  98. <div className="flex-1 min-w-0">
  99. <div className={`text-sm truncate transition-colors ${isSelected ? 'text-blue-700 font-semibold' : 'text-slate-700 font-medium'}`}>
  100. {group.name}
  101. </div>
  102. <div className={`text-xs mt-0.5 transition-colors ${isSelected ? 'text-blue-500/80' : 'text-slate-400'}`}>
  103. {group.fileCount} 个文件
  104. </div>
  105. </div>
  106. </div>
  107. );
  108. })}
  109. {filteredGroups.length === 0 && (
  110. <div className="py-12 text-center text-slate-400 text-sm flex flex-col items-center justify-center gap-2">
  111. <Database size={32} className="text-slate-200" />
  112. <span>{searchTerm ? t('noGroupsFound') : t('noGroups')}</span>
  113. </div>
  114. )}
  115. </div>
  116. <div className="p-4 border-t border-gray-200 bg-gray-50">
  117. <button
  118. onClick={onClose}
  119. className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
  120. >
  121. {t('done')} ({isAllSelected ? t('all') : selectedGroups.length})
  122. </button>
  123. </div>
  124. </div>
  125. </div>
  126. </div>,
  127. document.body
  128. );
  129. };