new-chat.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { useEffect, useRef, useState } from "react";
  2. import { Path, SlotID } from "../constant";
  3. import { IconButton } from "./button";
  4. import { EmojiAvatar } from "./emoji";
  5. import styles from "./new-chat.module.scss";
  6. import LeftIcon from "../icons/left.svg";
  7. import { useNavigate } from "react-router-dom";
  8. import { createEmptyMask, Mask, useMaskStore } from "../store/mask";
  9. import { useWindowSize } from "../utils";
  10. import { useChatStore } from "../store";
  11. import { MaskAvatar } from "./mask";
  12. function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
  13. const xmin = Math.max(aRect.x, bRect.x);
  14. const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
  15. const ymin = Math.max(aRect.y, bRect.y);
  16. const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
  17. const width = xmax - xmin;
  18. const height = ymax - ymin;
  19. const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
  20. return intersectionArea;
  21. }
  22. function MaskItem(props: { mask: Mask; onClick?: () => void }) {
  23. const domRef = useRef<HTMLDivElement>(null);
  24. useEffect(() => {
  25. const changeOpacity = () => {
  26. const dom = domRef.current;
  27. const parent = document.getElementById(SlotID.AppBody);
  28. if (!parent || !dom) return;
  29. const domRect = dom.getBoundingClientRect();
  30. const parentRect = parent.getBoundingClientRect();
  31. const intersectionArea = getIntersectionArea(domRect, parentRect);
  32. const domArea = domRect.width * domRect.height;
  33. const ratio = intersectionArea / domArea;
  34. const opacity = ratio > 0.9 ? 1 : 0.4;
  35. dom.style.opacity = opacity.toString();
  36. };
  37. setTimeout(changeOpacity, 30);
  38. window.addEventListener("resize", changeOpacity);
  39. return () => window.removeEventListener("resize", changeOpacity);
  40. }, [domRef]);
  41. return (
  42. <div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
  43. <MaskAvatar mask={props.mask} />
  44. <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
  45. </div>
  46. );
  47. }
  48. function useMaskGroup(masks: Mask[]) {
  49. const [groups, setGroups] = useState<Mask[][]>([]);
  50. useEffect(() => {
  51. const appBody = document.getElementById(SlotID.AppBody);
  52. if (!appBody) return;
  53. const rect = appBody.getBoundingClientRect();
  54. const maxWidth = rect.width;
  55. const maxHeight = rect.height * 0.6;
  56. const maskItemWidth = 120;
  57. const maskItemHeight = 50;
  58. const randomMask = () => masks[Math.floor(Math.random() * masks.length)];
  59. let maskIndex = 0;
  60. const nextMask = () => masks[maskIndex++ % masks.length];
  61. const rows = Math.ceil(maxHeight / maskItemHeight);
  62. const cols = Math.ceil(maxWidth / maskItemWidth);
  63. const newGroups = new Array(rows)
  64. .fill(0)
  65. .map((_, _i) =>
  66. new Array(cols)
  67. .fill(0)
  68. .map((_, j) => (j < 1 || j > cols - 2 ? randomMask() : nextMask())),
  69. );
  70. setGroups(newGroups);
  71. // eslint-disable-next-line react-hooks/exhaustive-deps
  72. }, []);
  73. return groups;
  74. }
  75. export function NewChat() {
  76. const chatStore = useChatStore();
  77. const maskStore = useMaskStore();
  78. const masks = maskStore.getAll();
  79. const groups = useMaskGroup(masks);
  80. const navigate = useNavigate();
  81. const startChat = (mask?: Mask) => {
  82. chatStore.newSession(mask);
  83. navigate(Path.Chat);
  84. };
  85. return (
  86. <div className={styles["new-chat"]}>
  87. <div className={styles["mask-header"]}>
  88. <IconButton
  89. icon={<LeftIcon />}
  90. text="返回"
  91. onClick={() => navigate(-1)}
  92. ></IconButton>
  93. <IconButton text="跳过" onClick={() => startChat()}></IconButton>
  94. </div>
  95. <div className={styles["mask-cards"]}>
  96. <div className={styles["mask-card"]}>
  97. <EmojiAvatar avatar="1f606" size={24} />
  98. </div>
  99. <div className={styles["mask-card"]}>
  100. <EmojiAvatar avatar="1f916" size={24} />
  101. </div>
  102. <div className={styles["mask-card"]}>
  103. <EmojiAvatar avatar="1f479" size={24} />
  104. </div>
  105. </div>
  106. <div className={styles["title"]}>挑选一个面具</div>
  107. <div className={styles["sub-title"]}>
  108. 现在开始,与面具背后的灵魂思维碰撞
  109. </div>
  110. <input
  111. className={styles["search-bar"]}
  112. placeholder="搜索"
  113. type="text"
  114. onClick={() => navigate(Path.Masks)}
  115. />
  116. <div className={styles["masks"]}>
  117. {groups.map((masks, i) => (
  118. <div key={i} className={styles["mask-row"]}>
  119. {masks.map((mask, index) => (
  120. <MaskItem key={index} mask={mask} onClick={startChat} />
  121. ))}
  122. </div>
  123. ))}
  124. </div>
  125. </div>
  126. );
  127. }