GroupSelectionDrawer.tsx 6.6 KB

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