page.tsx 8.4 KB

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