ConversationListItem.tsx 3.8 KB

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