ConversationListItem.tsx 3.5 KB

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