page.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. "use client";
  2. import { useRef, useState, useEffect } from "react";
  3. import { useRouter } from "next/navigation";
  4. import {
  5. RiSearch2Line,
  6. RiAddCircleFill,
  7. RiCalendarCloseLine,
  8. RiLoader2Fill,
  9. } from "react-icons/ri";
  10. import { useGlobalContext } from "@/providers/GlobalProvider";
  11. import { getCategoryList, getEmployeeSearch } from "@/api/client";
  12. import { CategoryListResult, EmployeeSearchResult } from "@/utils/clientsApis";
  13. import {
  14. Storage_setListType,
  15. Storage_setAgentId,
  16. Storage_setConversationId,
  17. } from "@/utils/chat";
  18. export default function Home() {
  19. const router = useRouter();
  20. const boxRef = useRef<HTMLDivElement | null>(null);
  21. const { changeMenuIndex } = useGlobalContext();
  22. const [categoryList, setCategoryList] = useState<CategoryListResult[]>([]);
  23. const [employeeList, setEmployeeList] = useState<EmployeeSearchResult[]>([]);
  24. const [categoryId, setCategoryId] = useState<number>(0);
  25. const [keyword, setKeyword] = useState<string>("");
  26. const [page, setPage] = useState<number>(1);
  27. const [isBottom, setIsBottom] = useState<boolean>(false);
  28. const page_getEmployeeSearch = () => {
  29. getEmployeeSearch({
  30. page: page,
  31. pageSize: 20,
  32. categoryId: categoryId,
  33. title: keyword,
  34. }).then((data) => {
  35. if (data.code === 0 && data.data.data !== null) {
  36. setEmployeeList((prevEmployeeList) => [
  37. ...prevEmployeeList,
  38. ...data.data.data, // Append new data to the existing list
  39. ]);
  40. if (data.data.total <= 20) {
  41. setIsBottom(true);
  42. }
  43. } else {
  44. setIsBottom(true);
  45. }
  46. });
  47. };
  48. const handleScroll = () => {
  49. if (
  50. boxRef.current &&
  51. boxRef.current.scrollHeight - boxRef.current.clientHeight ===
  52. boxRef.current.scrollTop
  53. ) {
  54. setPage((prevPage) => {
  55. const nextPage = prevPage + 1;
  56. page_getEmployeeSearch(); // Load the next page
  57. return nextPage;
  58. });
  59. }
  60. };
  61. const selectCategory = (id: number) => {
  62. if (categoryId != id) {
  63. setIsBottom(false);
  64. setCategoryId(id);
  65. setPage(1);
  66. setEmployeeList([]);
  67. }
  68. };
  69. const handleKeywordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  70. setKeyword(event.target.value);
  71. };
  72. const handleSearch = () => {
  73. setIsBottom(false);
  74. setPage(1);
  75. setEmployeeList([]);
  76. };
  77. const addChat = (id: number) => {
  78. Storage_setListType(1);
  79. Storage_setAgentId(id);
  80. Storage_setConversationId("");
  81. router.push("/dialogue");
  82. };
  83. const [isFocused, setIsFocused] = useState(false);
  84. const handleFocus = () => setIsFocused(true);
  85. const handleBlur = () => setIsFocused(false);
  86. useEffect(() => {
  87. changeMenuIndex(2);
  88. getCategoryList({ page: 1, pageSize: 20 }).then((data) => {
  89. if (data.code === 0) {
  90. setCategoryList(data.data.data);
  91. }
  92. });
  93. page_getEmployeeSearch();
  94. setPage(page + 1);
  95. }, [isBottom, categoryId, keyword]);
  96. return (
  97. <div
  98. className="absolute top-0 w-full overflow-auto scroll-smooth h-calc-8 md:h-calc-5"
  99. onScroll={handleScroll}
  100. ref={boxRef}
  101. >
  102. <div className="sticky top-0 z-50 dark:shadow-[rgba(255,255,255,.15)] backdrop-blur dark:bg-transparent transition-all">
  103. <div className="flex flex-row justify-center pt-9">
  104. <div className="w-11/12 md:basis-1/2 2xl:basis-1/3 relative">
  105. <input
  106. className="w-full border rounded-full pl-4 pr-12 py-2 focus:border-[#0061ff] dark:focus:border-slate-700 focus:outline-none"
  107. type="text"
  108. placeholder="输入关键词查询智能体..."
  109. value={keyword}
  110. onChange={handleKeywordChange}
  111. onFocus={handleFocus}
  112. onBlur={handleBlur}
  113. onKeyDown={(e) => {
  114. if (e.key === "Enter") {
  115. handleSearch();
  116. }
  117. }}
  118. />
  119. <div
  120. className={`absolute right-0 top-0 py-2 mr-4 cursor-pointer ${
  121. isFocused ? "text-[#0061ff]" : "text-gray-400"
  122. }`}
  123. >
  124. <RiSearch2Line size={24} onClick={handleSearch} />
  125. </div>
  126. </div>
  127. </div>
  128. {categoryList !== null && categoryList.length > 0 && (
  129. <div className="flex flex-row justify-center py-4">
  130. <div className="w-full md:container mx-auto flex flex-wrap justify-center gap-2 text-sm">
  131. <div className="w-full md:hidden overflow-x-auto scroll-smooth focus:scroll-auto">
  132. <div className="flex space-x-2 p-3">
  133. <button
  134. type="button"
  135. className={`px-4 py-1 border rounded-full whitespace-nowrap ${
  136. categoryId == 0
  137. ? "bg-blue-50 dark:bg-blue-950 text-[#0061ff]"
  138. : "text-gray-500 dark:text-gray-500 bg-white dark:bg-gray-800 hover:text-gray-900 dark:hover:text-white hover:bg-blue-50 dark:hover:bg-gray-800"
  139. }`}
  140. onClick={() => selectCategory(0)}
  141. >
  142. 全部分类
  143. </button>
  144. {categoryList.map((item) => (
  145. <button
  146. key={item.id}
  147. type="button"
  148. className={`px-4 py-1 border rounded-full whitespace-nowrap ${
  149. categoryId == item.id
  150. ? "bg-blue-50 dark:bg-blue-950 text-[#0061ff]"
  151. : "text-gray-500 dark:text-gray-500 bg-white dark:bg-gray-800 hover:text-gray-900 dark:hover:text-white hover:bg-blue-50 dark:hover:bg-gray-800"
  152. }`}
  153. onClick={() => selectCategory(item.id)}
  154. >
  155. {item.name}
  156. </button>
  157. ))}
  158. </div>
  159. </div>
  160. <div className="hidden md:flex flex-wrap justify-center gap-2">
  161. <button
  162. type="button"
  163. className={`px-4 py-1 border rounded-full ${
  164. categoryId == 0
  165. ? "bg-blue-50 dark:bg-blue-950 text-[#0061ff]"
  166. : "text-gray-500 dark:text-gray-500 bg-white dark:bg-gray-800 hover:text-gray-900 dark:hover:text-white hover:bg-blue-50 dark:hover:bg-gray-800"
  167. }`}
  168. onClick={() => selectCategory(0)}
  169. >
  170. 全部分类
  171. </button>
  172. {categoryList.map((item) => (
  173. <button
  174. key={item.id}
  175. type="button"
  176. className={`px-4 py-1 border rounded-full ${
  177. categoryId == item.id
  178. ? "bg-blue-50 dark:bg-blue-950 text-[#0061ff]"
  179. : "text-gray-500 dark:text-gray-500 bg-white dark:bg-gray-800 hover:text-gray-900 dark:hover:text-white hover:bg-blue-50 dark:hover:bg-gray-800"
  180. }`}
  181. onClick={() => selectCategory(item.id)}
  182. >
  183. {item.name}
  184. </button>
  185. ))}
  186. </div>
  187. </div>
  188. </div>
  189. )}
  190. </div>
  191. {employeeList !== null && employeeList.length > 0 && (
  192. <section className="text-gray-600 body-font">
  193. <div className="container px-5 py-5 md:py-20 mx-auto">
  194. <div className="flex flex-wrap -m-4 text-sm">
  195. {employeeList.map((item) => (
  196. <div
  197. className="p-4 lg:w-1/2 xl:w-1/3 transition transform hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:transform-none grow"
  198. key={item.id}
  199. >
  200. <a
  201. onClick={() => addChat(item.id)}
  202. className="relative block overflow-hidden rounded-lg border border-gray-100 dark:border-slate-700 p-4 sm:p-6 lg:p-8 shadow-md cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-950"
  203. >
  204. <span className="absolute inset-x-0 bottom-0 h-2 bg-gradient-to-r from-[#0061ff] via-[#347bec] to-[#6c62fd]"></span>
  205. <div className="flex justify-between gap-4">
  206. <div>
  207. <h3 className="text-lg font-bold text-gray-900 dark:text-white sm:text-xl text-ellipsis line-clamp-1 h-8">
  208. {item.title}
  209. </h3>
  210. <p className="mt-1 text-xs font-medium text-gray-600 text-ellipsis line-clamp-2 h-8">
  211. {item.skill}
  212. </p>
  213. </div>
  214. <div className="shrink-0">
  215. <img
  216. alt={item.title}
  217. src={item.avatar}
  218. className="size-16 object-contain"
  219. />
  220. </div>
  221. </div>
  222. <div className="mt-4">
  223. <p className="text-pretty text-sm text-gray-500 text-ellipsis line-clamp-4 h-20">
  224. {item.estimate}
  225. </p>
  226. </div>
  227. </a>
  228. </div>
  229. ))}
  230. </div>
  231. </div>
  232. {isBottom && (
  233. <div className="w-full mx-auto text-center text-sm text-gray-400 dark:text-gray-600 pb-20 md:pb-6">
  234. 已显示全部智能体
  235. </div>
  236. )}
  237. </section>
  238. )}
  239. {employeeList === null ||
  240. (employeeList.length === 0 && (
  241. <div className="w-full px-5 text-gray-200 flex flex-col mt-20">
  242. <div className="flex justify-center">
  243. <RiCalendarCloseLine className="w-16 h-16 text-center" />
  244. </div>
  245. <div className="text-center mt-4">查询结果为空</div>
  246. </div>
  247. ))}
  248. </div>
  249. );
  250. }