SentCart.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { Badge, Button, List, Popover, Tooltip, Typography } from "antd";
  2. import { useEffect, useRef, useState } from "react";
  3. import { ShoppingCartOutlined, DeleteOutlined } from "@ant-design/icons";
  4. import "./style.css";
  5. const { Text } = Typography;
  6. export interface ISentCart {
  7. id: string;
  8. text: string;
  9. }
  10. const SentCartWidget = () => {
  11. const [sentences, setSentences] = useState<ISentCart[]>([]);
  12. const sentencesRef = useRef(sentences);
  13. // Keep ref in sync so the interval callback always sees latest value
  14. useEffect(() => {
  15. sentencesRef.current = sentences;
  16. }, [sentences]);
  17. // Derive count directly — no need for a separate state
  18. const count = sentences.length || undefined;
  19. // Sync sentences → localStorage whenever it changes
  20. useEffect(() => {
  21. localStorage.setItem("cart/text", JSON.stringify(sentences));
  22. }, [sentences]);
  23. // Poll localStorage for external changes (e.g. another tab / component)
  24. useEffect(() => {
  25. const syncFromStorage = () => {
  26. const raw = localStorage.getItem("cart/text");
  27. const next: ISentCart[] = raw ? JSON.parse(raw) : [];
  28. // Only call setState when the data actually changed to avoid extra renders
  29. if (JSON.stringify(next) !== JSON.stringify(sentencesRef.current)) {
  30. setSentences(next);
  31. }
  32. };
  33. syncFromStorage(); // initial load
  34. const timer = setInterval(syncFromStorage, 2000);
  35. return () => clearInterval(timer);
  36. }, []); // empty deps — runs once, uses ref for comparison
  37. return (
  38. <>
  39. <Popover
  40. placement="bottomRight"
  41. arrow={{ pointAtCenter: true }}
  42. destroyOnHidden
  43. getTooltipContainer={() =>
  44. document.getElementsByClassName("toolbar_center")[0] as HTMLElement
  45. }
  46. content={
  47. <div>
  48. <div style={{ display: "flex", justifyContent: "space-between" }}>
  49. <div>{"复制句子编号"}</div>
  50. <div>
  51. <Text
  52. disabled={sentences.length === 0}
  53. copyable={{
  54. text: sentences.map((item) => item.id).join("\n"),
  55. }}
  56. />
  57. <Tooltip title="清空列表保留剪贴板数据">
  58. <Button
  59. disabled={sentences.length === 0}
  60. type="link"
  61. danger
  62. icon={<DeleteOutlined />}
  63. onClick={() => setSentences([])}
  64. />
  65. </Tooltip>
  66. </div>
  67. </div>
  68. <div style={{ width: 450, height: 300, overflowY: "auto" }}>
  69. <List
  70. size="small"
  71. dataSource={sentences}
  72. renderItem={(item, index) => (
  73. <List.Item key={item.id} className="cart_item">
  74. <List.Item.Meta title={item.id} description={item.text} />
  75. <Button
  76. className="cart_delete"
  77. type="link"
  78. danger
  79. icon={<DeleteOutlined />}
  80. onClick={() => {
  81. setSentences((prev) =>
  82. prev.filter((_, i) => i !== index)
  83. );
  84. }}
  85. />
  86. </List.Item>
  87. )}
  88. />
  89. </div>
  90. </div>
  91. }
  92. trigger="click"
  93. >
  94. <Badge style={{ cursor: "pointer" }} count={count} size="small">
  95. <span style={{ color: "white", cursor: "pointer" }}>
  96. <ShoppingCartOutlined />
  97. </span>
  98. </Badge>
  99. </Popover>
  100. </>
  101. );
  102. };
  103. export default SentCartWidget;