Files
aurak/web/components/GroupSelectionDrawer.tsx
Developer 0a9588abb7 feat: implement QuestionBank CRUD with pagination and template query
- 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
2026-04-23 17:19:11 +08:00

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
);
};