SearchHistoryList.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import React, { useState, useEffect } from 'react';
  2. import { SearchHistoryItem, KnowledgeGroup } from '../types';
  3. import { searchHistoryService } from '../services/searchHistoryService';
  4. import { useToast } from '../contexts/ToastContext';
  5. import { useLanguage } from '../contexts/LanguageContext';
  6. import { useConfirm } from '../contexts/ConfirmContext';
  7. import { MessageCircle, Trash2, Clock, Users } from 'lucide-react';
  8. interface SearchHistoryListProps {
  9. groups: KnowledgeGroup[];
  10. onSelectHistory: (historyId: string) => void;
  11. onDeleteHistory?: (historyId: string) => void;
  12. }
  13. export const SearchHistoryList: React.FC<SearchHistoryListProps> = ({
  14. groups,
  15. onSelectHistory,
  16. onDeleteHistory
  17. }) => {
  18. const [histories, setHistories] = useState<SearchHistoryItem[]>([]);
  19. const [loading, setLoading] = useState(true);
  20. const [page, setPage] = useState(1);
  21. const [hasMore, setHasMore] = useState(true);
  22. const { showError, showSuccess } = useToast();
  23. const { confirm } = useConfirm();
  24. const { t, language } = useLanguage();
  25. const loadHistories = async (pageNum: number = 1, append: boolean = false) => {
  26. try {
  27. setLoading(true);
  28. const response = await searchHistoryService.getHistories(pageNum, 20);
  29. if (append) {
  30. setHistories(prev => [...prev, ...response.histories]);
  31. } else {
  32. setHistories(response.histories);
  33. }
  34. setHasMore(response.histories.length === 20);
  35. setPage(pageNum);
  36. } catch (error) {
  37. showError(t('loadingHistoriesFailed'));
  38. } finally {
  39. setLoading(false);
  40. }
  41. };
  42. useEffect(() => {
  43. loadHistories();
  44. }, []);
  45. const handleDelete = async (historyId: string, e: React.MouseEvent) => {
  46. e.stopPropagation();
  47. if (!(await confirm(t('confirmDeleteHistory')))) return;
  48. try {
  49. await searchHistoryService.deleteHistory(historyId);
  50. setHistories(prev => prev.filter(h => h.id !== historyId));
  51. onDeleteHistory?.(historyId);
  52. showSuccess(t('deleteHistorySuccess'));
  53. } catch (error) {
  54. showError(t('deleteHistoryFailed'));
  55. }
  56. };
  57. const loadMore = () => {
  58. if (!loading && hasMore) {
  59. loadHistories(page + 1, true);
  60. }
  61. };
  62. const formatDate = (dateString: string) => {
  63. const date = new Date(dateString);
  64. const now = new Date();
  65. const diffMs = now.getTime() - date.getTime();
  66. const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  67. // Determine locale for standard date functions
  68. const localeMap: Record<string, string> = {
  69. 'zh': 'zh-CN',
  70. 'en': 'en-US',
  71. 'ja': 'ja-JP'
  72. };
  73. const locale = localeMap[language] || 'ja-JP';
  74. if (diffDays === 0) {
  75. return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
  76. } else if (diffDays === 1) {
  77. return t('yesterday');
  78. } else if (diffDays < 7) {
  79. return t('daysAgo', diffDays);
  80. } else {
  81. return date.toLocaleDateString(locale);
  82. }
  83. };
  84. const getGroupNames = (selectedGroups: string[] | null) => {
  85. if (!selectedGroups || selectedGroups.length === 0) {
  86. return t('allKnowledgeGroups');
  87. }
  88. return selectedGroups
  89. .map(id => groups.find(g => g.id === id)?.name)
  90. .filter(Boolean)
  91. .join(', ');
  92. };
  93. if (loading && histories.length === 0) {
  94. return (
  95. <div className="flex items-center justify-center py-8">
  96. <div className="text-gray-500">{t('loading')}</div>
  97. </div>
  98. );
  99. }
  100. if (histories.length === 0) {
  101. return (
  102. <div className="text-center py-8">
  103. <MessageCircle size={48} className="mx-auto text-gray-300 mb-4" />
  104. <div className="text-gray-500">{t('noHistory')}</div>
  105. <div className="text-sm text-gray-400 mt-1">{t('noHistoryDesc')}</div>
  106. </div>
  107. );
  108. }
  109. return (
  110. <div className="space-y-2">
  111. {histories.map((history) => (
  112. <div
  113. key={history.id}
  114. onClick={() => onSelectHistory(history.id)}
  115. className="p-4 bg-white rounded-lg border hover:shadow-sm cursor-pointer transition-all group"
  116. >
  117. <div className="flex items-start justify-between">
  118. <div className="flex-1 min-w-0">
  119. <div className="font-medium text-gray-900 truncate mb-1">
  120. {history.title}
  121. </div>
  122. <div className="flex items-center space-x-4 text-sm text-gray-500 mb-2">
  123. <div className="flex items-center space-x-1">
  124. <MessageCircle size={14} />
  125. <span>{t('historyMessages', history.messageCount)}</span>
  126. </div>
  127. <div className="flex items-center space-x-1">
  128. <Clock size={14} />
  129. <span>{formatDate(history.lastMessageAt)}</span>
  130. </div>
  131. </div>
  132. <div className="flex items-center space-x-1 text-xs text-gray-400">
  133. <Users size={12} />
  134. <span>{getGroupNames(history.selectedGroups)}</span>
  135. </div>
  136. </div>
  137. <button
  138. onClick={(e) => handleDelete(history.id, e)}
  139. className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-600 transition-all"
  140. >
  141. <Trash2 size={16} />
  142. </button>
  143. </div>
  144. </div>
  145. ))}
  146. {hasMore && (
  147. <button
  148. onClick={loadMore}
  149. disabled={loading}
  150. className="w-full py-3 text-center text-blue-600 hover:text-blue-700 disabled:opacity-50 transition-colors"
  151. >
  152. {loading ? t('loading') : t('loadMore')}
  153. </button>
  154. )}
  155. </div>
  156. );
  157. };