0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
143 lines
7.1 KiB
TypeScript
143 lines
7.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import { KnowledgeGroup } from '../types';
|
|
import { Check, X, Search, Database } from 'lucide-react';
|
|
|
|
interface GroupSelectionDrawerProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
groups: KnowledgeGroup[];
|
|
selectedGroups: string[];
|
|
onSelectionChange: (groupIds: string[]) => void;
|
|
}
|
|
|
|
export const GroupSelectionDrawer: React.FC<GroupSelectionDrawerProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
groups,
|
|
selectedGroups,
|
|
onSelectionChange
|
|
}) => {
|
|
const { t } = useLanguage();
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const filteredGroups = groups.filter(g =>
|
|
g.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const isAllSelected = selectedGroups.length === 0;
|
|
|
|
const handleToggleGroup = (groupId: string) => {
|
|
if (selectedGroups.includes(groupId)) {
|
|
onSelectionChange(selectedGroups.filter(id => id !== groupId));
|
|
} else {
|
|
onSelectionChange([...selectedGroups, groupId]);
|
|
}
|
|
};
|
|
|
|
const handleSelectAll = () => {
|
|
onSelectionChange([]);
|
|
};
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-50 overflow-hidden">
|
|
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm transition-opacity" onClick={onClose} />
|
|
<div className="absolute inset-y-0 right-0 max-w-md w-full flex">
|
|
<div className="flex-1 flex flex-col bg-white shadow-xl animate-in slide-in-from-right duration-300">
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
|
|
<h2 className="text-lg font-medium text-gray-900 flex items-center gap-2">
|
|
<Database size={20} />
|
|
{t('selectKnowledgeGroups')}
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search Box */}
|
|
<div className="p-4 border-b border-gray-100">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder={t('searchGroupsPlaceholder')}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-1">
|
|
{!searchTerm && (
|
|
<div
|
|
onClick={handleSelectAll}
|
|
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'
|
|
}`}
|
|
>
|
|
<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'
|
|
}`}>
|
|
{isAllSelected && <Check size={14} className="text-white" />}
|
|
</div>
|
|
<span className="font-semibold text-sm">{t('all')}</span>
|
|
</div>
|
|
)}
|
|
|
|
{filteredGroups.map((group) => {
|
|
const isSelected = selectedGroups.includes(group.id);
|
|
return (
|
|
<div
|
|
key={group.id}
|
|
onClick={() => handleToggleGroup(group.id)}
|
|
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'
|
|
}`}
|
|
>
|
|
<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'
|
|
}`}>
|
|
{isSelected && <Check size={14} className="text-white" />}
|
|
</div>
|
|
<div
|
|
className="w-3 h-3 rounded-full mr-3 flex-shrink-0 shadow-sm"
|
|
style={{ backgroundColor: group.color }}
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<div className={`text-sm truncate transition-colors ${isSelected ? 'text-blue-700 font-semibold' : 'text-slate-700 font-medium'}`}>
|
|
{group.name}
|
|
</div>
|
|
<div className={`text-xs mt-0.5 transition-colors ${isSelected ? 'text-blue-500/80' : 'text-slate-400'}`}>
|
|
{group.fileCount} 个文件
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{filteredGroups.length === 0 && (
|
|
<div className="py-12 text-center text-slate-400 text-sm flex flex-col items-center justify-center gap-2">
|
|
<Database size={32} className="text-slate-200" />
|
|
<span>{searchTerm ? t('noGroupsFound') : t('noGroups')}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-4 border-t border-gray-200 bg-gray-50">
|
|
<button
|
|
onClick={onClose}
|
|
className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
|
>
|
|
{t('done')} ({isAllSelected ? t('all') : selectedGroups.length})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
);
|
|
};
|