SearchHistoryList.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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 { MessageCircle, Trash2, Clock, Users } from 'lucide-react';
  6. interface SearchHistoryListProps {
  7. groups: KnowledgeGroup[];
  8. onSelectHistory: (historyId: string) => void;
  9. onDeleteHistory?: (historyId: string) => void;
  10. }
  11. export const SearchHistoryList: React.FC<SearchHistoryListProps> = ({
  12. groups,
  13. onSelectHistory,
  14. onDeleteHistory
  15. }) => {
  16. const [histories, setHistories] = useState<SearchHistoryItem[]>([]);
  17. const [loading, setLoading] = useState(true);
  18. const [page, setPage] = useState(1);
  19. const [hasMore, setHasMore] = useState(true);
  20. const { showToast } = useToast();
  21. const loadHistories = async (pageNum: number = 1, append: boolean = false) => {
  22. try {
  23. setLoading(true);
  24. const response = await searchHistoryService.getHistories(pageNum, 20);
  25. if (append) {
  26. setHistories(prev => [...prev, ...response.histories]);
  27. } else {
  28. setHistories(response.histories);
  29. }
  30. setHasMore(response.histories.length === 20);
  31. setPage(pageNum);
  32. } catch (error) {
  33. showToast('加载搜索历史失败', 'error');
  34. } finally {
  35. setLoading(false);
  36. }
  37. };
  38. useEffect(() => {
  39. loadHistories();
  40. }, []);
  41. const handleDelete = async (historyId: string, e: React.MouseEvent) => {
  42. e.stopPropagation();
  43. if (!confirm('确定要删除这条对话历史吗?')) return;
  44. try {
  45. await searchHistoryService.deleteHistory(historyId);
  46. setHistories(prev => prev.filter(h => h.id !== historyId));
  47. onDeleteHistory?.(historyId);
  48. showToast('对话历史删除成功', 'success');
  49. } catch (error) {
  50. showToast('删除对话历史失败', 'error');
  51. }
  52. };
  53. const loadMore = () => {
  54. if (!loading && hasMore) {
  55. loadHistories(page + 1, true);
  56. }
  57. };
  58. const formatDate = (dateString: string) => {
  59. const date = new Date(dateString);
  60. const now = new Date();
  61. const diffMs = now.getTime() - date.getTime();
  62. const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  63. if (diffDays === 0) {
  64. return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
  65. } else if (diffDays === 1) {
  66. return '昨天';
  67. } else if (diffDays < 7) {
  68. return `${diffDays}天前`;
  69. } else {
  70. return date.toLocaleDateString('zh-CN');
  71. }
  72. };
  73. const getGroupNames = (selectedGroups: string[] | null) => {
  74. if (!selectedGroups || selectedGroups.length === 0) {
  75. return '全部分组';
  76. }
  77. return selectedGroups
  78. .map(id => groups.find(g => g.id === id)?.name)
  79. .filter(Boolean)
  80. .join(', ');
  81. };
  82. if (loading && histories.length === 0) {
  83. return (
  84. <div className="flex items-center justify-center py-8">
  85. <div className="text-gray-500">加载中...</div>
  86. </div>
  87. );
  88. }
  89. if (histories.length === 0) {
  90. return (
  91. <div className="text-center py-8">
  92. <MessageCircle size={48} className="mx-auto text-gray-300 mb-4" />
  93. <div className="text-gray-500">暂无对话历史</div>
  94. <div className="text-sm text-gray-400 mt-1">开始一次对话来创建历史记录</div>
  95. </div>
  96. );
  97. }
  98. return (
  99. <div className="space-y-2">
  100. {histories.map((history) => (
  101. <div
  102. key={history.id}
  103. onClick={() => onSelectHistory(history.id)}
  104. className="p-4 bg-white rounded-lg border hover:shadow-sm cursor-pointer transition-all group"
  105. >
  106. <div className="flex items-start justify-between">
  107. <div className="flex-1 min-w-0">
  108. <div className="font-medium text-gray-900 truncate mb-1">
  109. {history.title}
  110. </div>
  111. <div className="flex items-center space-x-4 text-sm text-gray-500 mb-2">
  112. <div className="flex items-center space-x-1">
  113. <MessageCircle size={14} />
  114. <span>{history.messageCount} 条消息</span>
  115. </div>
  116. <div className="flex items-center space-x-1">
  117. <Clock size={14} />
  118. <span>{formatDate(history.lastMessageAt)}</span>
  119. </div>
  120. </div>
  121. <div className="flex items-center space-x-1 text-xs text-gray-400">
  122. <Users size={12} />
  123. <span>{getGroupNames(history.selectedGroups)}</span>
  124. </div>
  125. </div>
  126. <button
  127. onClick={(e) => handleDelete(history.id, e)}
  128. className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-600 transition-all"
  129. >
  130. <Trash2 size={16} />
  131. </button>
  132. </div>
  133. </div>
  134. ))}
  135. {hasMore && (
  136. <button
  137. onClick={loadMore}
  138. disabled={loading}
  139. className="w-full py-3 text-center text-blue-600 hover:text-blue-700 disabled:opacity-50 transition-colors"
  140. >
  141. {loading ? '加载中...' : '加载更多'}
  142. </button>
  143. )}
  144. </div>
  145. );
  146. };