index.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <template>
  2. <div class="search-results">
  3. <div class="search-box">
  4. <div class="input-wrapper">
  5. <input
  6. v-model="searchQuery"
  7. type="text"
  8. placeholder="Enter your search query"
  9. @keyup.enter="handleSearch"
  10. />
  11. <span
  12. v-if="searchQuery"
  13. class="clear-icon"
  14. @click="clearSearch"
  15. >×</span>
  16. </div>
  17. <button @click="handleSearch" :disabled="isSearching">
  18. 搜索一下
  19. </button>
  20. </div>
  21. <div class="results-header">
  22. <h2>Related Search Results</h2>
  23. <div class="result-stats">
  24. Found {{ total }} results
  25. </div>
  26. </div>
  27. <div class="results-container" ref="resultsContainer" @scroll="handleScroll">
  28. <!-- 搜索结果列表 -->
  29. <div v-if="displayResults.length > 0" class="results-list">
  30. <div v-for="(item, index) in displayResults" :key="index" class="result-item">
  31. <h3 class="result-title">
  32. <a :href="item.link" target="_blank" rel="noopener noreferrer">{{ item.title }}</a>
  33. </h3>
  34. <p class="result-url">{{ item.link }}</p>
  35. <p class="result-snippet">{{ item.snippet }}</p>
  36. <div class="result-meta">
  37. <span class="result-date">{{ formatDate(item.date) }}</span>
  38. <span class="result-source">{{ item.source }}</span>
  39. </div>
  40. </div>
  41. <!-- 加载更多指示器 -->
  42. <div v-if="hasMore" class="loading-more" ref="loadingTrigger">
  43. <div v-if="loading" class="loading-spinner"></div>
  44. <span v-else>Loading more results...</span>
  45. </div>
  46. </div>
  47. <!-- 无结果显示 -->
  48. <div v-else class="no-results">
  49. <FileSearchOutlined class="no-results-icon" />
  50. <p>No related results found</p>
  51. </div>
  52. </div>
  53. </div>
  54. </template>
  55. <script>
  56. import { defineComponent, ref, watch } from 'vue';
  57. import { FileSearchOutlined } from '@ant-design/icons-vue';
  58. import dayjs from 'dayjs';
  59. import axios from 'axios';
  60. export default defineComponent({
  61. name: 'SearchResults',
  62. components: {
  63. FileSearchOutlined
  64. },
  65. props: {
  66. searchResults: {
  67. type: Array,
  68. default: () => []
  69. },
  70. loading: {
  71. type: Boolean,
  72. default: false
  73. },
  74. total: {
  75. type: Number,
  76. default: 0
  77. },
  78. message: {
  79. type: String,
  80. default: ''
  81. },
  82. enableWebSearch: {
  83. type: Boolean,
  84. default: true,
  85. },
  86. },
  87. emits: ['update:total', 'update:enableWebSearch'],
  88. setup(props, { emit }) {
  89. const displayResults = ref([]);
  90. const hasMore = ref(false);
  91. const searchQuery = ref('');
  92. const isSearching = ref(false);
  93. const localTotal = ref(props.total);
  94. const isSearchEnabled = ref(props.enableWebSearch);
  95. // 监听props中的total变化
  96. watch(() => props.total, (newTotal) => {
  97. localTotal.value = newTotal;
  98. });
  99. // 监听 searchResults 变化
  100. watch(() => props.searchResults, (newResults) => {
  101. displayResults.value = newResults;
  102. hasMore.value = displayResults.value.length < props.total;
  103. }, { immediate: true });
  104. // 监听 message prop 的变化
  105. watch(() => props.message, (newMessage) => {
  106. if (newMessage && props.enableWebSearch) {
  107. searchQuery.value = newMessage;
  108. handleSearch(); // 自动触发搜索
  109. }
  110. });
  111. // 添加监听器
  112. watch(
  113. () => props.enableWebSearch,
  114. (newVal) => {
  115. isSearchEnabled.value = newVal;
  116. if (!newVal) {
  117. // 当搜索被关闭时,清空搜索结果
  118. clearSearch();
  119. }
  120. }
  121. );
  122. // 格式化日期
  123. const formatDate = (date) => {
  124. return dayjs(date).format('YYYY-MM-DD');
  125. };
  126. // 更新搜索处理函数
  127. const handleSearch = async () => {
  128. if (!searchQuery.value.trim() || isSearching.value || !props.enableWebSearch) return;
  129. isSearching.value = true;
  130. try {
  131. const response = await axios.post(
  132. `${import.meta.env.VITE_BASE_AI_API}/api/web-search-results/`,
  133. {
  134. query: searchQuery.value,
  135. num_results: 20,
  136. page: 1,
  137. page_size: 20,
  138. engine: "bing",
  139. }
  140. );
  141. // 添加调试日志
  142. console.log('API Response:', response.data);
  143. if (response.data && response.data.code === 200) {
  144. displayResults.value = response.data.data.results || [];
  145. // 添加调试日志
  146. console.log('Total Results:', response.data.data.total_results);
  147. localTotal.value = response.data.data.total_results || 0;
  148. emit('update:total', response.data.data.total_results || 0);
  149. }
  150. } catch (error) {
  151. console.error('Web search failed:', error);
  152. displayResults.value = [];
  153. localTotal.value = 0;
  154. emit('update:total', 0);
  155. } finally {
  156. isSearching.value = false;
  157. }
  158. };
  159. // 清空搜索函数
  160. const clearSearch = () => {
  161. searchQuery.value = '';
  162. displayResults.value = [];
  163. localTotal.value = 0;
  164. emit('update:total', 0);
  165. };
  166. const toggleSearch = () => {
  167. isSearchEnabled.value = !isSearchEnabled.value;
  168. emit('update:enableWebSearch', isSearchEnabled.value);
  169. };
  170. return {
  171. displayResults,
  172. loading: props.loading,
  173. total: localTotal,
  174. hasMore,
  175. formatDate,
  176. searchQuery,
  177. isSearching,
  178. handleSearch,
  179. clearSearch,
  180. isSearchEnabled,
  181. toggleSearch,
  182. };
  183. }
  184. });
  185. </script>
  186. <style lang="less" scoped>
  187. .search-results {
  188. height: 100%;
  189. display: flex;
  190. flex-direction: column;
  191. background: #fff;
  192. width: 100%;
  193. overflow: hidden;
  194. .search-box {
  195. padding: 16px 24px;
  196. border-bottom: 1px solid #f0f0f0;
  197. display: flex;
  198. gap: 0;
  199. .input-wrapper {
  200. position: relative;
  201. flex: 1;
  202. display: flex;
  203. align-items: center;
  204. input {
  205. width: 100%;
  206. padding: 8px 30px 8px 12px;
  207. height: 24px;
  208. font-size: 16px;
  209. border: 2px solid #4e6ef2;
  210. border-right: none;
  211. border-radius: 4px 0 0 4px;
  212. &:focus {
  213. outline: none;
  214. }
  215. }
  216. .clear-icon {
  217. position: absolute;
  218. right: 8px;
  219. top: 50%;
  220. transform: translateY(-50%);
  221. cursor: pointer;
  222. color: #999;
  223. font-size: 16px;
  224. width: 16px;
  225. height: 16px;
  226. display: flex;
  227. align-items: center;
  228. justify-content: center;
  229. border-radius: 50%;
  230. &:hover {
  231. color: #666;
  232. background-color: rgba(0, 0, 0, 0.05);
  233. }
  234. }
  235. }
  236. button {
  237. padding: 0 20px;
  238. height: 44px;
  239. background: #4e6ef2;
  240. border: none;
  241. border-radius: 0 4px 4px 0;
  242. color: white;
  243. cursor: pointer;
  244. font-size: 16px;
  245. min-width: 108px;
  246. &:disabled {
  247. background: #7a96fa;
  248. cursor: not-allowed;
  249. }
  250. &:hover:not(:disabled) {
  251. background: #4662d9;
  252. }
  253. }
  254. }
  255. .results-header {
  256. padding: 16px 24px;
  257. border-bottom: 1px solid #f0f0f0;
  258. h2 {
  259. margin: 0;
  260. font-size: 18px;
  261. color: rgba(0, 0, 0, 0.85);
  262. }
  263. .result-stats {
  264. margin-top: 4px;
  265. font-size: 14px;
  266. color: rgba(0, 0, 0, 0.45);
  267. }
  268. }
  269. .results-container {
  270. flex: 1;
  271. overflow-y: auto;
  272. padding: 0 24px;
  273. scroll-behavior: smooth;
  274. }
  275. .results-list {
  276. .result-item {
  277. padding: 20px 0;
  278. border-bottom: 1px solid #f0f0f0;
  279. &:last-child {
  280. border-bottom: none;
  281. }
  282. .result-title {
  283. cursor: pointer;
  284. margin: 0 0 8px;
  285. font-size: 16px;
  286. a {
  287. color: #1677ff;
  288. text-decoration: none;
  289. &:hover {
  290. text-decoration: underline;
  291. }
  292. }
  293. }
  294. .result-url {
  295. color: #0ca678;
  296. font-size: 13px;
  297. margin-bottom: 8px;
  298. }
  299. .result-snippet {
  300. color: rgba(0, 0, 0, 0.65);
  301. font-size: 14px;
  302. margin-bottom: 8px;
  303. line-height: 1.5;
  304. }
  305. .result-meta {
  306. font-size: 12px;
  307. color: rgba(0, 0, 0, 0.45);
  308. .result-date {
  309. margin-right: 16px;
  310. }
  311. }
  312. }
  313. }
  314. .no-results {
  315. text-align: center;
  316. padding: 48px 0;
  317. color: rgba(0, 0, 0, 0.45);
  318. .no-results-icon {
  319. font-size: 48px;
  320. margin-bottom: 16px;
  321. }
  322. p {
  323. font-size: 14px;
  324. }
  325. }
  326. .loading-more {
  327. text-align: center;
  328. padding: 20px 0;
  329. color: rgba(0, 0, 0, 0.45);
  330. font-size: 14px;
  331. }
  332. .loading-spinner {
  333. display: inline-block;
  334. width: 20px;
  335. height: 20px;
  336. border: 2px solid #f3f3f3;
  337. border-top: 2px solid #1677ff;
  338. border-radius: 50%;
  339. animation: spin 1s linear infinite;
  340. }
  341. @keyframes spin {
  342. 0% { transform: rotate(0deg); }
  343. 100% { transform: rotate(360deg); }
  344. }
  345. // 添加滚动条样式
  346. .results-container::-webkit-scrollbar {
  347. width: 6px;
  348. }
  349. .results-container::-webkit-scrollbar-thumb {
  350. background-color: rgba(0, 0, 0, 0.2);
  351. border-radius: 3px;
  352. }
  353. .results-container::-webkit-scrollbar-track {
  354. background-color: transparent;
  355. }
  356. }
  357. </style>