forked from hangshuo652/aurak
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
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user