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,178 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SearchHistoryItem, KnowledgeGroup } from '../types';
|
||||
import { searchHistoryService } from '../services/searchHistoryService';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useConfirm } from '../contexts/ConfirmContext';
|
||||
import { MessageCircle, Trash2, Clock, Users } from 'lucide-react';
|
||||
|
||||
interface SearchHistoryListProps {
|
||||
groups: KnowledgeGroup[];
|
||||
onSelectHistory: (historyId: string) => void;
|
||||
onDeleteHistory?: (historyId: string) => void;
|
||||
}
|
||||
|
||||
export const SearchHistoryList: React.FC<SearchHistoryListProps> = ({
|
||||
groups,
|
||||
onSelectHistory,
|
||||
onDeleteHistory
|
||||
}) => {
|
||||
const [histories, setHistories] = useState<SearchHistoryItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const { showError, showSuccess } = useToast();
|
||||
const { confirm } = useConfirm();
|
||||
const { t, language } = useLanguage();
|
||||
|
||||
const loadHistories = async (pageNum: number = 1, append: boolean = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await searchHistoryService.getHistories(pageNum, 20);
|
||||
|
||||
if (append) {
|
||||
setHistories(prev => [...prev, ...response.histories]);
|
||||
} else {
|
||||
setHistories(response.histories);
|
||||
}
|
||||
|
||||
setHasMore(response.histories.length === 20);
|
||||
setPage(pageNum);
|
||||
} catch (error) {
|
||||
showError(t('loadingHistoriesFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadHistories();
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (historyId: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!(await confirm(t('confirmDeleteHistory')))) return;
|
||||
|
||||
try {
|
||||
await searchHistoryService.deleteHistory(historyId);
|
||||
setHistories(prev => prev.filter(h => h.id !== historyId));
|
||||
onDeleteHistory?.(historyId);
|
||||
showSuccess(t('deleteHistorySuccess'));
|
||||
} catch (error) {
|
||||
showError(t('deleteHistoryFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const loadMore = () => {
|
||||
if (!loading && hasMore) {
|
||||
loadHistories(page + 1, true);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Determine locale for standard date functions
|
||||
const localeMap: Record<string, string> = {
|
||||
'zh': 'zh-CN',
|
||||
'en': 'en-US',
|
||||
'ja': 'ja-JP'
|
||||
};
|
||||
const locale = localeMap[language] || 'ja-JP';
|
||||
|
||||
if (diffDays === 0) {
|
||||
return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (diffDays === 1) {
|
||||
return t('yesterday');
|
||||
} else if (diffDays < 7) {
|
||||
return t('daysAgo', diffDays);
|
||||
} else {
|
||||
return date.toLocaleDateString(locale);
|
||||
}
|
||||
};
|
||||
|
||||
const getGroupNames = (selectedGroups: string[] | null) => {
|
||||
if (!selectedGroups || selectedGroups.length === 0) {
|
||||
return t('allKnowledgeGroups');
|
||||
}
|
||||
return selectedGroups
|
||||
.map(id => groups.find(g => g.id === id)?.name)
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
if (loading && histories.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-gray-500">{t('loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (histories.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<MessageCircle size={48} className="mx-auto text-gray-300 mb-4" />
|
||||
<div className="text-gray-500">{t('noHistory')}</div>
|
||||
<div className="text-sm text-gray-400 mt-1">{t('noHistoryDesc')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{histories.map((history) => (
|
||||
<div
|
||||
key={history.id}
|
||||
onClick={() => onSelectHistory(history.id)}
|
||||
className="p-4 bg-white rounded-lg border hover:shadow-sm cursor-pointer transition-all group"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900 truncate mb-1">
|
||||
{history.title}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-500 mb-2">
|
||||
<div className="flex items-center space-x-1">
|
||||
<MessageCircle size={14} />
|
||||
<span>{t('historyMessages', history.messageCount)}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock size={14} />
|
||||
<span>{formatDate(history.lastMessageAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 text-xs text-gray-400">
|
||||
<Users size={12} />
|
||||
<span>{getGroupNames(history.selectedGroups)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={(e) => handleDelete(history.id, e)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-600 transition-all"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={loadMore}
|
||||
disabled={loading}
|
||||
className="w-full py-3 text-center text-blue-600 hover:text-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loading ? t('loading') : t('loadMore')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user