ConversationListItem.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. "use client";
  2. import { ConversationSummary } from "@/features/agent/types";
  3. import {
  4. buildMessagePreview,
  5. formatConversationTime,
  6. isVisitorOnline,
  7. } from "@/utils/format";
  8. interface ConversationListItemProps {
  9. conversation: ConversationSummary;
  10. selected: boolean;
  11. onSelect: (id: number) => void;
  12. }
  13. export function ConversationListItem({
  14. conversation,
  15. selected,
  16. onSelect,
  17. }: ConversationListItemProps) {
  18. const avatarColor = `hsl(${(conversation.id * 137.5) % 360}, 70%, 50%)`;
  19. const unreadCount = conversation.unread_count ?? 0;
  20. const lastMessage = conversation.last_message;
  21. const lastMessagePreview = lastMessage
  22. ? buildMessagePreview(lastMessage.content)
  23. : "暂无消息";
  24. // 根据 last_seen_at 判断是否在线(最近 10 秒内认为在线)
  25. const isOnline = isVisitorOnline(conversation.last_seen_at);
  26. return (
  27. <div
  28. onClick={(event) => {
  29. event.preventDefault();
  30. event.stopPropagation();
  31. onSelect(conversation.id);
  32. }}
  33. onMouseDown={(event) => {
  34. if (event.button === 0) {
  35. event.preventDefault();
  36. }
  37. }}
  38. className={`p-4 border-b border-gray-100 cursor-pointer transition-colors select-none ${
  39. selected ? "bg-blue-50 border-l-4 border-l-blue-500" : "hover:bg-gray-50"
  40. }`}
  41. >
  42. <div className="flex items-start gap-3">
  43. <div
  44. className="w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold text-sm flex-shrink-0"
  45. style={{ backgroundColor: avatarColor }}
  46. >
  47. {conversation.visitor_id.toString().slice(-2)}
  48. </div>
  49. <div className="flex-1 min-w-0">
  50. <div className="flex items-center gap-2 mb-1">
  51. <span className="font-medium text-gray-800 text-sm truncate">
  52. 对话 #{conversation.id}
  53. </span>
  54. {/* 在线/离线状态图标 */}
  55. {isOnline && (
  56. <span
  57. className="w-2 h-2 rounded-full flex-shrink-0"
  58. title="在线"
  59. style={{ backgroundColor: "#10b981" }}
  60. />
  61. )}
  62. {unreadCount > 0 && (
  63. <span className="px-1.5 py-0.5 rounded-full text-[10px] bg-blue-500 text-white flex-shrink-0">
  64. {unreadCount > 99 ? "99+" : unreadCount}
  65. </span>
  66. )}
  67. <span
  68. className={`px-2 py-0.5 rounded text-xs flex-shrink-0 ${
  69. conversation.status === "open"
  70. ? "bg-green-100 text-green-700"
  71. : "bg-gray-100 text-gray-700"
  72. }`}
  73. >
  74. {conversation.status === "open" ? "进行中" : "已关闭"}
  75. </span>
  76. </div>
  77. <div className="text-xs text-gray-600 mb-1 flex items-center gap-1">
  78. {lastMessage?.sender_is_agent && (
  79. <span
  80. className={`text-[10px] ${
  81. lastMessage.is_read ? "text-blue-400" : "text-gray-400"
  82. }`}
  83. >
  84. {lastMessage.is_read ? "✓✓" : "✓"}
  85. </span>
  86. )}
  87. <span className="truncate">{lastMessagePreview}</span>
  88. </div>
  89. <div className="flex items-center justify-between text-xs text-gray-400">
  90. <span>访客 #{conversation.visitor_id}</span>
  91. <span>{formatConversationTime(conversation.updated_at)}</span>
  92. </div>
  93. </div>
  94. </div>
  95. </div>
  96. );
  97. }