From fbfff981239b677534a3cedf22ded540a9ee7519 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Sat, 22 Mar 2025 22:53:37 -0400 Subject: [PATCH] [dev]update search --- src/components/SearchBar.jsx | 26 +-- src/components/Snackbar.jsx | 31 ++-- src/layouts/Mainlayout.jsx | 1 - src/pages/Chat/ChatWindow.jsx | 11 +- src/pages/Chat/NewChat.jsx | 19 +-- .../Detail/KnowledgeBaseDetail.jsx | 17 +- src/pages/KnowledgeBase/KnowledgeBase.jsx | 161 ++++++------------ src/pages/auth/Login.jsx | 43 +++-- src/router/router.jsx | 3 + src/services/api.js | 4 +- src/store/auth/auth.thunk.js | 10 +- .../knowledgeBase/knowledgeBase.thunks.js | 2 +- 12 files changed, 144 insertions(+), 184 deletions(-) diff --git a/src/components/SearchBar.jsx b/src/components/SearchBar.jsx index d0416ea..c6ae245 100644 --- a/src/components/SearchBar.jsx +++ b/src/components/SearchBar.jsx @@ -47,32 +47,23 @@ const SearchBar = ({ }; }, []); - // 当搜索框获得焦点且有关键词时显示下拉框 - const handleFocus = () => { - if (searchKeyword.trim().length > 0) { + // 只有在用户执行搜索后且有结果时显示下拉框 + useEffect(() => { + if (isSearching && searchResults.length > 0) { setShowDropdown(true); } - }; + }, [isSearching, searchResults]); // 处理输入变化 const handleInputChange = (e) => { - const value = e.target.value; onSearchChange(e); - - if (value.trim().length > 0) { - setShowDropdown(true); - } else { - setShowDropdown(false); - } }; // 处理搜索提交 const handleSubmit = (e) => { e.preventDefault(); onSearch(e); - if (searchKeyword.trim().length > 0) { - setShowDropdown(true); - } + // 搜索提交后,如果有关键词,将显示下拉框(由searchResults决定) }; return ( @@ -86,7 +77,6 @@ const SearchBar = ({ placeholder={placeholder} value={searchKeyword} onChange={handleInputChange} - onFocus={handleFocus} /> {searchKeyword.trim() && ( - - +
+ +
{message}
+ +
); }; diff --git a/src/layouts/Mainlayout.jsx b/src/layouts/Mainlayout.jsx index 341d730..76ef448 100644 --- a/src/layouts/Mainlayout.jsx +++ b/src/layouts/Mainlayout.jsx @@ -7,7 +7,6 @@ export default function Mainlayout({ children }) { return ( <> - {children} ); diff --git a/src/pages/Chat/ChatWindow.jsx b/src/pages/Chat/ChatWindow.jsx index 5cc540f..b3de737 100644 --- a/src/pages/Chat/ChatWindow.jsx +++ b/src/pages/Chat/ChatWindow.jsx @@ -18,10 +18,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { error: messagesError, } = useSelector((state) => state.chat.messages); const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage); - const knowledgeBase = useSelector((state) => - state.knowledgeBase.list.data?.items?.find((kb) => kb.id === knowledgeBaseId) - ); - const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.list.isLoading); + + // 使用新的Redux状态结构 + const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []); + const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId); + const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.loading); // 获取聊天消息 useEffect(() => { @@ -56,7 +57,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { // 从 Redux store 获取知识库信息 useEffect(() => { if (!knowledgeBase && !isLoadingKnowledgeBases) { - dispatch(fetchKnowledgeBases()); + dispatch(fetchKnowledgeBases({ page: 1, page_size: 50 })); } }, [dispatch, knowledgeBase, isLoadingKnowledgeBases]); diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx index 40fc7dc..c0ca35c 100644 --- a/src/pages/Chat/NewChat.jsx +++ b/src/pages/Chat/NewChat.jsx @@ -8,23 +8,20 @@ import SvgIcon from '../../components/SvgIcon'; export default function NewChat() { const navigate = useNavigate(); const dispatch = useDispatch(); - const [loading, setLoading] = useState(true); - // 从 Redux store 获取知识库数据 - const { data, status, error } = useSelector((state) => state.knowledgeBase.list); - const knowledgeBases = data?.items || []; - const isLoading = status === 'loading'; + // 从 Redux store 获取知识库数据 - 使用新的状态结构 + const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []); + const isLoading = useSelector((state) => state.knowledgeBase.loading); + const error = useSelector((state) => state.knowledgeBase.error); // 获取知识库列表 useEffect(() => { - if (!data?.items?.length && status !== 'loading') { - dispatch(fetchKnowledgeBases()); - } - }, [dispatch, data, status]); + dispatch(fetchKnowledgeBases({ page: 1, page_size: 50 })); + }, [dispatch]); // 监听错误状态 useEffect(() => { - if (status === 'failed' && error) { + if (error) { dispatch( showNotification({ message: `获取知识库列表失败: ${error.message || error}`, @@ -32,7 +29,7 @@ export default function NewChat() { }) ); } - }, [status, error, dispatch]); + }, [error, dispatch]); // 过滤出有 can_read 权限的知识库 const readableKnowledgeBases = knowledgeBases.filter((kb) => kb.permissions && kb.permissions.can_read === true); diff --git a/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx b/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx index 77d99b7..40b841f 100644 --- a/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx +++ b/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; import { showNotification } from '../../../store/notification.slice'; -import { getKnowledgeBaseById } from '../../../store/knowledgeBase/knowledgeBase.thunks'; +import { fetchKnowledgeBaseDetail } from '../../../store/knowledgeBase/knowledgeBase.thunks'; import SvgIcon from '../../../components/SvgIcon'; import DatasetTab from './DatasetTab'; import SettingsTab from './SettingsTab'; @@ -13,14 +13,15 @@ export default function KnowledgeBaseDetail() { const dispatch = useDispatch(); const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets'); - // Get knowledge base details from Redux store - const { data: knowledgeBase, status, error } = useSelector((state) => state.knowledgeBase.detail); - const isLoading = status === 'loading'; + // Get knowledge base details from Redux store - 使用新的状态结构 + const knowledgeBase = useSelector((state) => state.knowledgeBase.currentKnowledgeBase); + const loading = useSelector((state) => state.knowledgeBase.loading); + const error = useSelector((state) => state.knowledgeBase.error); // Fetch knowledge base details when component mounts or ID changes useEffect(() => { if (id) { - dispatch(getKnowledgeBaseById(id)); + dispatch(fetchKnowledgeBaseDetail(id)); } }, [dispatch, id]); @@ -33,7 +34,7 @@ export default function KnowledgeBaseDetail() { // If knowledge base not found, show notification and redirect useEffect(() => { - if (status === 'failed' && error) { + if (!loading && error) { dispatch( showNotification({ message: `获取知识库失败: ${error.message || '未找到知识库'}`, @@ -42,7 +43,7 @@ export default function KnowledgeBaseDetail() { ); navigate('/knowledge-base'); } - }, [status, error, dispatch, navigate]); + }, [loading, error, dispatch, navigate]); // Handle tab change const handleTabChange = (tab) => { @@ -51,7 +52,7 @@ export default function KnowledgeBaseDetail() { }; // Show loading state if knowledge base not loaded yet - if (isLoading || !knowledgeBase) { + if (loading || !knowledgeBase) { return (
diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index 0546a72..7876493 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -16,7 +16,6 @@ import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal' import Pagination from '../../components/Pagination'; import SearchBar from '../../components/SearchBar'; import ApiModeSwitch from '../../components/ApiModeSwitch'; -import debounce from 'lodash/debounce'; // 导入拆分的组件 import KnowledgeBaseList from './components/KnowledgeBaseList'; @@ -47,7 +46,7 @@ export default function KnowledgeBase() { // Search state const [searchKeyword, setSearchKeyword] = useState(''); - const [isSearching, setIsSearching] = useState(false); + const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false); // Pagination state const [pagination, setPagination] = useState({ @@ -68,100 +67,26 @@ export default function KnowledgeBase() { const searchResults = useSelector((state) => state.knowledgeBase.searchResults); const searchLoading = useSelector((state) => state.knowledgeBase.searchLoading); - // Determine which data to display based on search state - const displayData = isSearching ? searchResults : knowledgeBases; - const displayTotal = paginationData.total; - const displayStatus = loading ? 'loading' : 'succeeded'; - const displayError = error; - // Fetch knowledge bases when component mounts or pagination changes useEffect(() => { - if (!isSearching) { - dispatch(fetchKnowledgeBases(pagination)); - } else if (searchKeyword.trim()) { - dispatch( - searchKnowledgeBases({ - keyword: searchKeyword, - page: pagination.page, - page_size: pagination.page_size, - }) - ); - } - }, [dispatch, pagination.page, pagination.page_size, isSearching, searchKeyword]); - - // 实时搜索处理函数 - const debouncedSearch = useCallback( - debounce((keyword) => { - if (keyword.trim()) { - dispatch( - searchKnowledgeBases({ - keyword, - page: 1, - page_size: 5, - }) - ); - } else { - dispatch(clearSearchResults()); - } - }, 300), - [dispatch] - ); - - // Handle search input change - const handleSearchInputChange = (e) => { - const value = e.target.value; - setSearchKeyword(value); - - // 实时搜索 - if (value.trim()) { - debouncedSearch(value); - } else { - dispatch(clearSearchResults()); - } - }; - - // Handle search submit - const handleSearch = (e) => { - e.preventDefault(); - - if (searchKeyword.trim()) { - setIsSearching(true); - setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page - dispatch( - searchKnowledgeBases({ - keyword: searchKeyword, - page: 1, - page_size: pagination.page_size, - }) - ); - } else { - // If search is empty, reset to normal list view - handleClearSearch(); - } - }; - - // Handle clear search - const handleClearSearch = () => { - setSearchKeyword(''); - setIsSearching(false); - setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page - dispatch(clearSearchResults()); - }; + // 无论是否在搜索,都正常获取知识库列表 + dispatch(fetchKnowledgeBases(pagination)); + }, [dispatch, pagination.page, pagination.page_size]); // Show loading state while fetching data - const isLoading = displayStatus === 'loading'; + const isLoading = loading; // Show error notification if fetch fails useEffect(() => { - if (displayStatus === 'failed' && displayError) { + if (!isLoading && error) { dispatch( showNotification({ - message: `获取知识库列表失败: ${displayError.message || displayError}`, + message: `获取知识库列表失败: ${error.message || error}`, type: 'danger', }) ); } - }, [displayStatus, displayError, dispatch]); + }, [isLoading, error, dispatch]); // Show notification for operation status useEffect(() => { @@ -169,17 +94,7 @@ export default function KnowledgeBase() { // 操作成功通知由具体函数处理,这里只刷新列表 // Refresh the list after successful operation - if (isSearching && searchKeyword.trim()) { - dispatch( - searchKnowledgeBases({ - keyword: searchKeyword, - page: pagination.page, - page_size: pagination.page_size, - }) - ); - } else { - dispatch(fetchKnowledgeBases(pagination)); - } + dispatch(fetchKnowledgeBases(pagination)); } else if (operationStatus === 'failed' && operationError) { dispatch( showNotification({ @@ -188,7 +103,47 @@ export default function KnowledgeBase() { }) ); } - }, [operationStatus, operationError, dispatch, pagination, isSearching, searchKeyword]); + }, [operationStatus, operationError, dispatch, pagination]); + + // Handle search input change + const handleSearchInputChange = (e) => { + const value = e.target.value; + setSearchKeyword(value); + + // 如果搜索框清空,关闭下拉框 + if (!value.trim()) { + dispatch(clearSearchResults()); + setIsSearchDropdownOpen(false); + } + }; + + // Handle search submit - 只影响下拉框,不影响主列表 + const handleSearch = (e) => { + e.preventDefault(); + + if (searchKeyword.trim()) { + // 只设置下拉框搜索状态,不设置全局isSearching状态 + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: 1, + page_size: 5, // 下拉框只显示少量结果 + }) + ); + setIsSearchDropdownOpen(true); + } else { + // 清空搜索及关闭下拉框 + handleClearSearch(); + } + }; + + // Handle clear search + const handleClearSearch = () => { + setSearchKeyword(''); + // 不影响主列表显示,只关闭下拉框 + setIsSearchDropdownOpen(false); + dispatch(clearSearchResults()); + }; // Handle pagination change const handlePageChange = (newPage) => { @@ -426,7 +381,7 @@ export default function KnowledgeBase() { }; // Calculate total pages - const totalPages = Math.ceil(displayTotal / pagination.page_size); + const totalPages = Math.ceil(paginationData.total / pagination.page_size); // 打开创建知识库弹窗 const handleOpenCreateModal = () => { @@ -458,7 +413,7 @@ export default function KnowledgeBase() {
- {isSearching && ( -
- 搜索结果: "{searchKeyword}" - 找到 {displayTotal} 个知识库 -
- )} - {isLoading ? (
@@ -489,14 +438,14 @@ export default function KnowledgeBase() { ) : ( <> - {/* Pagination */} + {/* Pagination - 始终显示 */} {totalPages > 1 && ( state.auth); @@ -22,18 +24,20 @@ export default function Login() { try { await dispatch(checkAuthThunk()).unwrap(); if (user) navigate('/'); - } catch (error) {} + } catch (error) { + // 检查登录状态失败,不需要显示通知 + } }; const validateForm = () => { const newErrors = {}; if (!username) { - newErrors.username = 'Username is required'; + newErrors.username = '请输入用户名'; } if (!password) { - newErrors.password = 'Password is required'; + newErrors.password = '请输入密码'; } else if (password.length < 6) { - newErrors.password = 'Password must be at least 6 characters'; + newErrors.password = '密码长度不能少于6个字符'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -44,17 +48,19 @@ export default function Login() { setSubmitted(true); if (validateForm()) { - console.log('Form submitted successfully!'); - console.log('Username:', username); - console.log('Password:', password); + setIsLoading(true); try { await dispatch(loginThunk({ username, password })).unwrap(); navigate('/'); } catch (error) { + // 登录失败的错误通知已在thunk中处理 console.error('Login failed:', error); + } finally { + setIsLoading(false); } } }; + return (
OOIN 智能知识库
@@ -69,7 +75,7 @@ export default function Login() { type='text' className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`} id='username' - placeholder='Username' + placeholder='用户名' required onChange={(e) => setUsername(e.target.value.trim())} > @@ -80,7 +86,7 @@ export default function Login() { value={password} type='password' id='password' - placeholder='Password' + placeholder='密码' required className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`} aria-describedby='passwordHelpBlock' @@ -89,14 +95,25 @@ export default function Login() { {submitted && errors.password &&
{errors.password}
}
- Forgot password? + 忘记密码? - - Need Account? + 没有账号?去注册
); diff --git a/src/router/router.jsx b/src/router/router.jsx index d4dba8d..7e4f11a 100644 --- a/src/router/router.jsx +++ b/src/router/router.jsx @@ -10,6 +10,7 @@ import Login from '../pages/Auth/Login'; import Signup from '../pages/Auth/Signup'; import ProtectedRoute from './protectedRoute'; import { useSelector } from 'react-redux'; +import NotificationSnackbar from '../components/NotificationSnackbar'; function AppRouter() { const { user } = useSelector((state) => state.auth); @@ -19,6 +20,8 @@ function AppRouter() { return ( }> + + }> { console.log(`[MOCK MODE] GET ${url}`); return await mockGet(url, params); } - - const res = await api.get(url, { params }); + + const res = await api.get(url, { ...params }); return res.data; } catch (error) { if (!hasCheckedServer || (error.request && !error.response)) { diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js index 34137f0..f23d814 100644 --- a/src/store/auth/auth.thunk.js +++ b/src/store/auth/auth.thunk.js @@ -10,9 +10,12 @@ export const loginThunk = createAsyncThunk( 'auth/login', async ({ username, password }, { rejectWithValue, dispatch }) => { try { - const { message, data } = await post('/auth/login/', { username, password }); - console.log('data', data); - + const { message, data, code } = await post('/auth/login/', { username, password }); + console.log('code', code); + + if (code !== 200) { + throw new Error(message || 'Something went wrong'); + } if (!data) { throw new Error(message || 'Something went wrong'); } @@ -24,6 +27,7 @@ export const loginThunk = createAsyncThunk( return data; } catch (error) { const errorMessage = error.response?.data?.message || 'Something went wrong'; + console.log(errorMessage); dispatch( showNotification({ message: errorMessage, diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js index 18ca463..97621d9 100644 --- a/src/store/knowledgeBase/knowledgeBase.thunks.js +++ b/src/store/knowledgeBase/knowledgeBase.thunks.js @@ -34,7 +34,7 @@ export const fetchKnowledgeBases = createAsyncThunk( export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => { try { const { keyword, page = 1, page_size = 10 } = params; - const response = await get('/knowledge-bases/search/', { + const response = await get('/knowledge-bases/search', { params: { keyword, page,