mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 05:09:44 +08:00
[dev]update search
This commit is contained in:
parent
167b06315d
commit
fbfff98123
@ -47,32 +47,23 @@ const SearchBar = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 当搜索框获得焦点且有关键词时显示下拉框
|
// 只有在用户执行搜索后且有结果时显示下拉框
|
||||||
const handleFocus = () => {
|
useEffect(() => {
|
||||||
if (searchKeyword.trim().length > 0) {
|
if (isSearching && searchResults.length > 0) {
|
||||||
setShowDropdown(true);
|
setShowDropdown(true);
|
||||||
}
|
}
|
||||||
};
|
}, [isSearching, searchResults]);
|
||||||
|
|
||||||
// 处理输入变化
|
// 处理输入变化
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const value = e.target.value;
|
|
||||||
onSearchChange(e);
|
onSearchChange(e);
|
||||||
|
|
||||||
if (value.trim().length > 0) {
|
|
||||||
setShowDropdown(true);
|
|
||||||
} else {
|
|
||||||
setShowDropdown(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理搜索提交
|
// 处理搜索提交
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSearch(e);
|
onSearch(e);
|
||||||
if (searchKeyword.trim().length > 0) {
|
// 搜索提交后,如果有关键词,将显示下拉框(由searchResults决定)
|
||||||
setShowDropdown(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -86,7 +77,6 @@ const SearchBar = ({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onFocus={handleFocus}
|
|
||||||
/>
|
/>
|
||||||
{searchKeyword.trim() && (
|
{searchKeyword.trim() && (
|
||||||
<button
|
<button
|
||||||
@ -107,9 +97,9 @@ const SearchBar = ({
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* 搜索结果下拉框 */}
|
{/* 搜索结果下拉框 - 仅在用户搜索且有结果时显示 */}
|
||||||
{showDropdown && (
|
{showDropdown && (isSearchLoading || searchResults?.length > 0) && (
|
||||||
<div className='position-absolute bg-white shadow-sm rounded-3 mt-1 w-100 search-results-dropdown'>
|
<div className='position-absolute bg-white shadow-sm rounded-3 mt-1 w-100 search-results-dropdown z-1'>
|
||||||
<div className='p-2 overflow-auto' style={{ maxHeight: '350px', zIndex: '1050' }}>
|
<div className='p-2 overflow-auto' style={{ maxHeight: '350px', zIndex: '1050' }}>
|
||||||
{isSearchLoading ? (
|
{isSearchLoading ? (
|
||||||
<div className='text-center p-3'>
|
<div className='text-center p-3'>
|
||||||
|
@ -11,7 +11,7 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
|
|||||||
}, duration);
|
}, duration);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [duration, onClose]);
|
}, [message, duration, onClose]);
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
success: 'check-circle-fill',
|
success: 'check-circle-fill',
|
||||||
@ -20,22 +20,21 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
|
|||||||
danger: 'exclamation-triangle-fill',
|
danger: 'exclamation-triangle-fill',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理关闭按钮点击
|
||||||
|
const handleClose = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onClose) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 z-2 gap-2`}
|
||||||
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 alert-dismissible z-2 gap-2`}
|
role='alert'
|
||||||
role='alert'
|
>
|
||||||
>
|
<SvgIcon className={icons[type]} />
|
||||||
<SvgIcon className={icons[type]} />
|
<div className='flex-fill'>{message}</div>
|
||||||
<div className='flex-fill'>{message}</div>
|
<button type='button' className='btn-close flex-end' onClick={handleClose} aria-label='Close'></button>
|
||||||
<button
|
</div>
|
||||||
type='button'
|
|
||||||
className='btn-close flex-end'
|
|
||||||
data-bs-dismiss='alert'
|
|
||||||
aria-label='Close'
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ export default function Mainlayout({ children }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderWithNav />
|
<HeaderWithNav />
|
||||||
<NotificationSnackbar />
|
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -18,10 +18,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
error: messagesError,
|
error: messagesError,
|
||||||
} = useSelector((state) => state.chat.messages);
|
} = useSelector((state) => state.chat.messages);
|
||||||
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
|
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
|
||||||
const knowledgeBase = useSelector((state) =>
|
|
||||||
state.knowledgeBase.list.data?.items?.find((kb) => kb.id === knowledgeBaseId)
|
// 使用新的Redux状态结构
|
||||||
);
|
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
|
||||||
const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.list.isLoading);
|
const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId);
|
||||||
|
const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.loading);
|
||||||
|
|
||||||
// 获取聊天消息
|
// 获取聊天消息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -56,7 +57,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
// 从 Redux store 获取知识库信息
|
// 从 Redux store 获取知识库信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!knowledgeBase && !isLoadingKnowledgeBases) {
|
if (!knowledgeBase && !isLoadingKnowledgeBases) {
|
||||||
dispatch(fetchKnowledgeBases());
|
dispatch(fetchKnowledgeBases({ page: 1, page_size: 50 }));
|
||||||
}
|
}
|
||||||
}, [dispatch, knowledgeBase, isLoadingKnowledgeBases]);
|
}, [dispatch, knowledgeBase, isLoadingKnowledgeBases]);
|
||||||
|
|
||||||
|
@ -8,23 +8,20 @@ import SvgIcon from '../../components/SvgIcon';
|
|||||||
export default function NewChat() {
|
export default function NewChat() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// 从 Redux store 获取知识库数据
|
// 从 Redux store 获取知识库数据 - 使用新的状态结构
|
||||||
const { data, status, error } = useSelector((state) => state.knowledgeBase.list);
|
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
|
||||||
const knowledgeBases = data?.items || [];
|
const isLoading = useSelector((state) => state.knowledgeBase.loading);
|
||||||
const isLoading = status === 'loading';
|
const error = useSelector((state) => state.knowledgeBase.error);
|
||||||
|
|
||||||
// 获取知识库列表
|
// 获取知识库列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data?.items?.length && status !== 'loading') {
|
dispatch(fetchKnowledgeBases({ page: 1, page_size: 50 }));
|
||||||
dispatch(fetchKnowledgeBases());
|
}, [dispatch]);
|
||||||
}
|
|
||||||
}, [dispatch, data, status]);
|
|
||||||
|
|
||||||
// 监听错误状态
|
// 监听错误状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'failed' && error) {
|
if (error) {
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: `获取知识库列表失败: ${error.message || error}`,
|
message: `获取知识库列表失败: ${error.message || error}`,
|
||||||
@ -32,7 +29,7 @@ export default function NewChat() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [status, error, dispatch]);
|
}, [error, dispatch]);
|
||||||
|
|
||||||
// 过滤出有 can_read 权限的知识库
|
// 过滤出有 can_read 权限的知识库
|
||||||
const readableKnowledgeBases = knowledgeBases.filter((kb) => kb.permissions && kb.permissions.can_read === true);
|
const readableKnowledgeBases = knowledgeBases.filter((kb) => kb.permissions && kb.permissions.can_read === true);
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { showNotification } from '../../../store/notification.slice';
|
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 SvgIcon from '../../../components/SvgIcon';
|
||||||
import DatasetTab from './DatasetTab';
|
import DatasetTab from './DatasetTab';
|
||||||
import SettingsTab from './SettingsTab';
|
import SettingsTab from './SettingsTab';
|
||||||
@ -13,14 +13,15 @@ export default function KnowledgeBaseDetail() {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
|
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
|
||||||
|
|
||||||
// Get knowledge base details from Redux store
|
// Get knowledge base details from Redux store - 使用新的状态结构
|
||||||
const { data: knowledgeBase, status, error } = useSelector((state) => state.knowledgeBase.detail);
|
const knowledgeBase = useSelector((state) => state.knowledgeBase.currentKnowledgeBase);
|
||||||
const isLoading = status === 'loading';
|
const loading = useSelector((state) => state.knowledgeBase.loading);
|
||||||
|
const error = useSelector((state) => state.knowledgeBase.error);
|
||||||
|
|
||||||
// Fetch knowledge base details when component mounts or ID changes
|
// Fetch knowledge base details when component mounts or ID changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
dispatch(getKnowledgeBaseById(id));
|
dispatch(fetchKnowledgeBaseDetail(id));
|
||||||
}
|
}
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ export default function KnowledgeBaseDetail() {
|
|||||||
|
|
||||||
// If knowledge base not found, show notification and redirect
|
// If knowledge base not found, show notification and redirect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'failed' && error) {
|
if (!loading && error) {
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: `获取知识库失败: ${error.message || '未找到知识库'}`,
|
message: `获取知识库失败: ${error.message || '未找到知识库'}`,
|
||||||
@ -42,7 +43,7 @@ export default function KnowledgeBaseDetail() {
|
|||||||
);
|
);
|
||||||
navigate('/knowledge-base');
|
navigate('/knowledge-base');
|
||||||
}
|
}
|
||||||
}, [status, error, dispatch, navigate]);
|
}, [loading, error, dispatch, navigate]);
|
||||||
|
|
||||||
// Handle tab change
|
// Handle tab change
|
||||||
const handleTabChange = (tab) => {
|
const handleTabChange = (tab) => {
|
||||||
@ -51,7 +52,7 @@ export default function KnowledgeBaseDetail() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Show loading state if knowledge base not loaded yet
|
// Show loading state if knowledge base not loaded yet
|
||||||
if (isLoading || !knowledgeBase) {
|
if (loading || !knowledgeBase) {
|
||||||
return (
|
return (
|
||||||
<div className='container-fluid px-4 py-5 text-center'>
|
<div className='container-fluid px-4 py-5 text-center'>
|
||||||
<div className='spinner-border' role='status'>
|
<div className='spinner-border' role='status'>
|
||||||
|
@ -16,7 +16,6 @@ import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal'
|
|||||||
import Pagination from '../../components/Pagination';
|
import Pagination from '../../components/Pagination';
|
||||||
import SearchBar from '../../components/SearchBar';
|
import SearchBar from '../../components/SearchBar';
|
||||||
import ApiModeSwitch from '../../components/ApiModeSwitch';
|
import ApiModeSwitch from '../../components/ApiModeSwitch';
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
|
|
||||||
// 导入拆分的组件
|
// 导入拆分的组件
|
||||||
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
||||||
@ -47,7 +46,7 @@ export default function KnowledgeBase() {
|
|||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
@ -68,100 +67,26 @@ export default function KnowledgeBase() {
|
|||||||
const searchResults = useSelector((state) => state.knowledgeBase.searchResults);
|
const searchResults = useSelector((state) => state.knowledgeBase.searchResults);
|
||||||
const searchLoading = useSelector((state) => state.knowledgeBase.searchLoading);
|
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
|
// Fetch knowledge bases when component mounts or pagination changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSearching) {
|
// 无论是否在搜索,都正常获取知识库列表
|
||||||
dispatch(fetchKnowledgeBases(pagination));
|
dispatch(fetchKnowledgeBases(pagination));
|
||||||
} else if (searchKeyword.trim()) {
|
}, [dispatch, pagination.page, pagination.page_size]);
|
||||||
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());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show loading state while fetching data
|
// Show loading state while fetching data
|
||||||
const isLoading = displayStatus === 'loading';
|
const isLoading = loading;
|
||||||
|
|
||||||
// Show error notification if fetch fails
|
// Show error notification if fetch fails
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (displayStatus === 'failed' && displayError) {
|
if (!isLoading && error) {
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: `获取知识库列表失败: ${displayError.message || displayError}`,
|
message: `获取知识库列表失败: ${error.message || error}`,
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [displayStatus, displayError, dispatch]);
|
}, [isLoading, error, dispatch]);
|
||||||
|
|
||||||
// Show notification for operation status
|
// Show notification for operation status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -169,17 +94,7 @@ export default function KnowledgeBase() {
|
|||||||
// 操作成功通知由具体函数处理,这里只刷新列表
|
// 操作成功通知由具体函数处理,这里只刷新列表
|
||||||
|
|
||||||
// Refresh the list after successful operation
|
// Refresh the list after successful operation
|
||||||
if (isSearching && searchKeyword.trim()) {
|
dispatch(fetchKnowledgeBases(pagination));
|
||||||
dispatch(
|
|
||||||
searchKnowledgeBases({
|
|
||||||
keyword: searchKeyword,
|
|
||||||
page: pagination.page,
|
|
||||||
page_size: pagination.page_size,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dispatch(fetchKnowledgeBases(pagination));
|
|
||||||
}
|
|
||||||
} else if (operationStatus === 'failed' && operationError) {
|
} else if (operationStatus === 'failed' && operationError) {
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
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
|
// Handle pagination change
|
||||||
const handlePageChange = (newPage) => {
|
const handlePageChange = (newPage) => {
|
||||||
@ -426,7 +381,7 @@ export default function KnowledgeBase() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Calculate total pages
|
// Calculate total pages
|
||||||
const totalPages = Math.ceil(displayTotal / pagination.page_size);
|
const totalPages = Math.ceil(paginationData.total / pagination.page_size);
|
||||||
|
|
||||||
// 打开创建知识库弹窗
|
// 打开创建知识库弹窗
|
||||||
const handleOpenCreateModal = () => {
|
const handleOpenCreateModal = () => {
|
||||||
@ -458,7 +413,7 @@ export default function KnowledgeBase() {
|
|||||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
isSearching={isSearching}
|
isSearching={isSearchDropdownOpen} // 用于控制下拉框显示
|
||||||
onSearchChange={handleSearchInputChange}
|
onSearchChange={handleSearchInputChange}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onClearSearch={handleClearSearch}
|
onClearSearch={handleClearSearch}
|
||||||
@ -474,12 +429,6 @@ export default function KnowledgeBase() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSearching && (
|
|
||||||
<div className='alert alert-info'>
|
|
||||||
搜索结果: "{searchKeyword}" - 找到 {displayTotal} 个知识库
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className='d-flex justify-content-center my-5'>
|
<div className='d-flex justify-content-center my-5'>
|
||||||
<div className='spinner-border' role='status'>
|
<div className='spinner-border' role='status'>
|
||||||
@ -489,14 +438,14 @@ export default function KnowledgeBase() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<KnowledgeBaseList
|
<KnowledgeBaseList
|
||||||
knowledgeBases={displayData}
|
knowledgeBases={knowledgeBases}
|
||||||
isSearching={isSearching}
|
isSearching={false} // 始终为false,因为搜索不影响主列表
|
||||||
onCardClick={handleCardClick}
|
onCardClick={handleCardClick}
|
||||||
onRequestAccess={handleRequestAccess}
|
onRequestAccess={handleRequestAccess}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination - 始终显示 */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={pagination.page}
|
currentPage={pagination.page}
|
||||||
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
|
import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
|
||||||
|
import { showNotification } from '../../store/notification.slice';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -10,6 +11,7 @@ export default function Login() {
|
|||||||
const [password, setPassword] = useState('leader123');
|
const [password, setPassword] = useState('leader123');
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { user } = useSelector((state) => state.auth);
|
const { user } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
@ -22,18 +24,20 @@ export default function Login() {
|
|||||||
try {
|
try {
|
||||||
await dispatch(checkAuthThunk()).unwrap();
|
await dispatch(checkAuthThunk()).unwrap();
|
||||||
if (user) navigate('/');
|
if (user) navigate('/');
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
// 检查登录状态失败,不需要显示通知
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors = {};
|
const newErrors = {};
|
||||||
if (!username) {
|
if (!username) {
|
||||||
newErrors.username = 'Username is required';
|
newErrors.username = '请输入用户名';
|
||||||
}
|
}
|
||||||
if (!password) {
|
if (!password) {
|
||||||
newErrors.password = 'Password is required';
|
newErrors.password = '请输入密码';
|
||||||
} else if (password.length < 6) {
|
} else if (password.length < 6) {
|
||||||
newErrors.password = 'Password must be at least 6 characters';
|
newErrors.password = '密码长度不能少于6个字符';
|
||||||
}
|
}
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
@ -44,17 +48,19 @@ export default function Login() {
|
|||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
|
|
||||||
if (validateForm()) {
|
if (validateForm()) {
|
||||||
console.log('Form submitted successfully!');
|
setIsLoading(true);
|
||||||
console.log('Username:', username);
|
|
||||||
console.log('Password:', password);
|
|
||||||
try {
|
try {
|
||||||
await dispatch(loginThunk({ username, password })).unwrap();
|
await dispatch(loginThunk({ username, password })).unwrap();
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 登录失败的错误通知已在thunk中处理
|
||||||
console.error('Login failed:', error);
|
console.error('Login failed:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
||||||
<div className='title text-center h1'>OOIN 智能知识库</div>
|
<div className='title text-center h1'>OOIN 智能知识库</div>
|
||||||
@ -69,7 +75,7 @@ export default function Login() {
|
|||||||
type='text'
|
type='text'
|
||||||
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
|
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
|
||||||
id='username'
|
id='username'
|
||||||
placeholder='Username'
|
placeholder='用户名'
|
||||||
required
|
required
|
||||||
onChange={(e) => setUsername(e.target.value.trim())}
|
onChange={(e) => setUsername(e.target.value.trim())}
|
||||||
></input>
|
></input>
|
||||||
@ -80,7 +86,7 @@ export default function Login() {
|
|||||||
value={password}
|
value={password}
|
||||||
type='password'
|
type='password'
|
||||||
id='password'
|
id='password'
|
||||||
placeholder='Password'
|
placeholder='密码'
|
||||||
required
|
required
|
||||||
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
|
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
|
||||||
aria-describedby='passwordHelpBlock'
|
aria-describedby='passwordHelpBlock'
|
||||||
@ -89,14 +95,25 @@ export default function Login() {
|
|||||||
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
|
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
|
||||||
</div>
|
</div>
|
||||||
<Link to='#' className='find-password text-body-secondary'>
|
<Link to='#' className='find-password text-body-secondary'>
|
||||||
Forgot password?
|
忘记密码?
|
||||||
</Link>
|
</Link>
|
||||||
<button type='submit' className='btn btn-dark btn-lg w-100'>
|
<button type='submit' className='btn btn-dark btn-lg w-100' disabled={isLoading}>
|
||||||
Login
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className='spinner-border spinner-border-sm me-2'
|
||||||
|
role='status'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
登录中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'登录'
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<Link to='/signup' className='go-to-signup w-100 link-underline-light h5 text-center'>
|
<Link to='/signup' className='go-to-signup w-100 link-underline-light h5 text-center'>
|
||||||
Need Account?
|
没有账号?去注册
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import Login from '../pages/Auth/Login';
|
|||||||
import Signup from '../pages/Auth/Signup';
|
import Signup from '../pages/Auth/Signup';
|
||||||
import ProtectedRoute from './protectedRoute';
|
import ProtectedRoute from './protectedRoute';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import NotificationSnackbar from '../components/NotificationSnackbar';
|
||||||
|
|
||||||
function AppRouter() {
|
function AppRouter() {
|
||||||
const { user } = useSelector((state) => state.auth);
|
const { user } = useSelector((state) => state.auth);
|
||||||
@ -19,6 +20,8 @@ function AppRouter() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
|
<NotificationSnackbar />
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<ProtectedRoute />}>
|
<Route element={<ProtectedRoute />}>
|
||||||
<Route
|
<Route
|
||||||
|
@ -100,8 +100,8 @@ const get = async (url, params = {}) => {
|
|||||||
console.log(`[MOCK MODE] GET ${url}`);
|
console.log(`[MOCK MODE] GET ${url}`);
|
||||||
return await mockGet(url, params);
|
return await mockGet(url, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.get(url, { params });
|
const res = await api.get(url, { ...params });
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
if (!hasCheckedServer || (error.request && !error.response)) {
|
||||||
|
@ -10,9 +10,12 @@ export const loginThunk = createAsyncThunk(
|
|||||||
'auth/login',
|
'auth/login',
|
||||||
async ({ username, password }, { rejectWithValue, dispatch }) => {
|
async ({ username, password }, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
const { message, data } = await post('/auth/login/', { username, password });
|
const { message, data, code } = await post('/auth/login/', { username, password });
|
||||||
console.log('data', data);
|
console.log('code', code);
|
||||||
|
|
||||||
|
if (code !== 200) {
|
||||||
|
throw new Error(message || 'Something went wrong');
|
||||||
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error(message || 'Something went wrong');
|
throw new Error(message || 'Something went wrong');
|
||||||
}
|
}
|
||||||
@ -24,6 +27,7 @@ export const loginThunk = createAsyncThunk(
|
|||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.message || 'Something went wrong';
|
const errorMessage = error.response?.data?.message || 'Something went wrong';
|
||||||
|
console.log(errorMessage);
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
|
@ -34,7 +34,7 @@ export const fetchKnowledgeBases = createAsyncThunk(
|
|||||||
export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => {
|
export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const { keyword, page = 1, page_size = 10 } = params;
|
const { keyword, page = 1, page_size = 10 } = params;
|
||||||
const response = await get('/knowledge-bases/search/', {
|
const response = await get('/knowledge-bases/search', {
|
||||||
params: {
|
params: {
|
||||||
keyword,
|
keyword,
|
||||||
page,
|
page,
|
||||||
|
Loading…
Reference in New Issue
Block a user