ConversationHeader.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. "use client";
  2. import { useState, useRef, useEffect } from "react";
  3. import { ChevronDown } from "lucide-react";
  4. export type ConversationFilter = "all" | "mine" | "others";
  5. export type ConversationListStatus = "open" | "closed";
  6. interface ConversationHeaderProps {
  7. filter: ConversationFilter;
  8. onFilterChange: (filter: ConversationFilter) => void;
  9. /** 与「全部对话」同一行右侧:进行中 / 历史 */
  10. listStatus?: ConversationListStatus;
  11. onListStatusChange?: (status: ConversationListStatus) => void;
  12. }
  13. const FILTER_OPTIONS: { value: ConversationFilter; label: string }[] = [
  14. { value: "all", label: "全部对话" },
  15. { value: "mine", label: "我的对话" },
  16. { value: "others", label: "他人对话" },
  17. ];
  18. export function ConversationHeader({
  19. filter,
  20. onFilterChange,
  21. listStatus,
  22. onListStatusChange,
  23. }: ConversationHeaderProps) {
  24. const [open, setOpen] = useState(false);
  25. const ref = useRef<HTMLDivElement>(null);
  26. const currentLabel = FILTER_OPTIONS.find((o) => o.value === filter)?.label ?? "全部对话";
  27. useEffect(() => {
  28. const handleClickOutside = (e: MouseEvent) => {
  29. if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
  30. };
  31. if (open) document.addEventListener("mousedown", handleClickOutside);
  32. return () => document.removeEventListener("mousedown", handleClickOutside);
  33. }, [open]);
  34. const showListStatus =
  35. listStatus !== undefined && typeof onListStatusChange === "function";
  36. return (
  37. <div className="h-14 flex items-center gap-2 px-3 border-b border-border bg-background flex-shrink-0 min-w-0">
  38. <div className="relative min-w-0 shrink" ref={ref}>
  39. <button
  40. type="button"
  41. onClick={() => setOpen((v) => !v)}
  42. className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-border bg-background hover:bg-muted/50 text-sm font-medium text-foreground transition-colors max-w-[9.5rem] min-w-0"
  43. >
  44. <span className="truncate">{currentLabel}</span>
  45. <ChevronDown
  46. className={`w-4 h-4 text-muted-foreground flex-shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
  47. />
  48. </button>
  49. {open && (
  50. <div className="absolute top-full left-0 mt-1 py-1 rounded-lg border border-border bg-popover shadow-md z-50 min-w-[theme(spacing.32)]">
  51. {FILTER_OPTIONS.map((opt) => (
  52. <button
  53. key={opt.value}
  54. type="button"
  55. onClick={() => {
  56. onFilterChange(opt.value);
  57. setOpen(false);
  58. }}
  59. className={`w-full px-3 py-2 text-left text-sm transition-colors ${
  60. filter === opt.value
  61. ? "bg-primary/10 text-primary font-medium"
  62. : "text-foreground hover:bg-muted/50"
  63. }`}
  64. >
  65. {opt.label}
  66. </button>
  67. ))}
  68. </div>
  69. )}
  70. </div>
  71. {showListStatus && (
  72. <div className="ml-auto flex-shrink-0">
  73. <div className="inline-flex rounded-md border border-border/70 bg-muted/30 p-0.5">
  74. <button
  75. type="button"
  76. className={`px-2.5 py-1 rounded-[0.25rem] text-[11px] font-medium transition leading-none ${
  77. listStatus === "open"
  78. ? "bg-green-600 text-white shadow-sm"
  79. : "text-muted-foreground hover:text-foreground"
  80. }`}
  81. onClick={() => onListStatusChange!("open")}
  82. >
  83. 进行中
  84. </button>
  85. <button
  86. type="button"
  87. className={`px-2.5 py-1 rounded-[0.25rem] text-[11px] font-medium transition leading-none ${
  88. listStatus === "closed"
  89. ? "bg-green-600 text-white shadow-sm"
  90. : "text-muted-foreground hover:text-foreground"
  91. }`}
  92. onClick={() => onListStatusChange!("closed")}
  93. >
  94. 历史
  95. </button>
  96. </div>
  97. </div>
  98. )}
  99. </div>
  100. );
  101. }