KnowledgeBase_frontend/src/pages/KnowledgeBase/KnowledgeBase.jsx

536 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { showNotification } from '../../store/notification.slice';
import {
fetchKnowledgeBases,
searchKnowledgeBases,
createKnowledgeBase,
deleteKnowledgeBase,
requestKnowledgeBaseAccess,
} from '../../store/knowledgeBase/knowledgeBase.thunks';
import { clearSearchResults } from '../../store/knowledgeBase/knowledgeBase.slice';
import SvgIcon from '../../components/SvgIcon';
import AccessRequestModal from '../../components/AccessRequestModal';
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';
export default function KnowledgeBase() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showAccessRequestModal, setShowAccessRequestModal] = useState(false);
const [formErrors, setFormErrors] = useState({});
const [accessRequestKnowledgeBase, setAccessRequestKnowledgeBase] = useState({
id: '',
title: '',
});
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
const [createdKnowledgeBaseId, setCreatedKnowledgeBaseId] = useState(null);
// 获取当前用户信息
const currentUser = useSelector((state) => state.auth.user);
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
name: '',
desc: '',
type: 'private', // 默认为私有知识库
department: currentUser?.department || '',
group: currentUser?.group || '',
});
// Search state
const [searchKeyword, setSearchKeyword] = useState('');
const [isSearching, setIsSearching] = useState(false);
// Pagination state
const [pagination, setPagination] = useState({
page: 1,
page_size: 10,
});
// Get knowledge bases from Redux store
// 更新为新的Redux状态结构
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases);
const loading = useSelector((state) => state.knowledgeBase.loading);
const paginationData = useSelector((state) => state.knowledgeBase.pagination);
const error = useSelector((state) => state.knowledgeBase.error);
const operationStatus = useSelector((state) => state.knowledgeBase.editStatus);
const operationError = useSelector((state) => state.knowledgeBase.error);
// 从Redux获取搜索结果和加载状态
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());
};
// Show loading state while fetching data
const isLoading = displayStatus === 'loading';
// Show error notification if fetch fails
useEffect(() => {
if (displayStatus === 'failed' && displayError) {
dispatch(
showNotification({
message: `获取知识库列表失败: ${displayError.message || displayError}`,
type: 'danger',
})
);
}
}, [displayStatus, displayError, dispatch]);
// Show notification for operation status
useEffect(() => {
if (operationStatus === 'successful') {
// 操作成功通知由具体函数处理,这里只刷新列表
// 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));
}
} else if (operationStatus === 'failed' && operationError) {
dispatch(
showNotification({
message: `操作失败: ${operationError.message || operationError}`,
type: 'danger',
})
);
}
}, [operationStatus, operationError, dispatch, pagination, isSearching, searchKeyword]);
// Handle pagination change
const handlePageChange = (newPage) => {
setPagination((prev) => ({
...prev,
page: newPage,
}));
};
// Handle page size change
const handlePageSizeChange = (newPageSize) => {
setPagination({
page: 1, // Reset to first page when changing page size
page_size: newPageSize,
});
};
const handleInputChange = (e) => {
const { name, value } = e.target;
// 不允许修改部门和组别字段
if (name === 'department' || name === 'group') {
return;
}
// 检查用户是否有权限选择指定的知识库类型
if (name === 'type') {
const role = currentUser?.role;
let allowed = false;
// 根据角色判断可以选择的知识库类型
if (role === 'admin') {
// 管理员可以选择任何类型
allowed = ['admin', 'leader', 'member', 'private', 'secret'].includes(value);
} else if (role === 'leader') {
// 组长只能选择 member 和 private
allowed = ['member', 'private'].includes(value);
} else {
// 普通成员只能选择 private
allowed = value === 'private';
}
if (!allowed) {
dispatch(
showNotification({
message: '您没有权限创建此类型的知识库',
type: 'warning',
})
);
return;
}
}
setNewKnowledgeBase((prev) => ({
...prev,
[name]: value,
}));
// Clear error when user types
if (formErrors[name]) {
setFormErrors((prev) => ({
...prev,
[name]: '',
}));
}
};
const validateCreateForm = () => {
const errors = {};
if (!newKnowledgeBase.name.trim()) {
errors.name = '请输入知识库名称';
}
if (!newKnowledgeBase.desc.trim()) {
errors.desc = '请输入知识库描述';
}
if (!newKnowledgeBase.type) {
errors.type = '请选择知识库类型';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
const handleCreateKnowledgeBase = async () => {
// Validate form
if (!validateCreateForm()) {
return;
}
try {
// Dispatch create knowledge base action
const resultAction = await dispatch(
createKnowledgeBase({
name: newKnowledgeBase.name,
desc: newKnowledgeBase.desc,
description: newKnowledgeBase.desc,
type: newKnowledgeBase.type,
department: newKnowledgeBase.department,
group: newKnowledgeBase.group,
})
);
console.log('创建知识库返回数据:', resultAction);
// Check if the action was successful
if (createKnowledgeBase.fulfilled.match(resultAction)) {
console.log('创建成功payload:', resultAction.payload);
const { knowledge_base } = resultAction.payload;
const { id } = knowledge_base;
// Get ID from payload and navigate
if (id) {
console.log('新知识库ID:', id);
// 显示成功通知
dispatch(
showNotification({
message: '知识库创建成功',
type: 'success',
})
);
// 直接导航到新创建的知识库详情页
navigate(`/knowledge-base/${id}`);
} else {
console.error('无法获取新知识库ID:', resultAction.payload);
dispatch(
showNotification({
message: '创建成功但无法获取知识库ID',
type: 'warning',
})
);
}
} else {
console.error('创建知识库失败:', resultAction.error);
dispatch(
showNotification({
message: `创建知识库失败: ${resultAction.error?.message || '未知错误'}`,
type: 'danger',
})
);
}
} catch (error) {
console.error('创建知识库出错:', error);
dispatch(
showNotification({
message: `创建知识库出错: ${error.message || '未知错误'}`,
type: 'danger',
})
);
}
// Reset form and close modal
setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
setFormErrors({});
setShowCreateModal(false);
};
// Handle card click to navigate to knowledge base detail
const handleCardClick = (id, permissions) => {
// 检查用户是否有读取权限
if (!permissions || permissions.can_read === false) {
dispatch(
showNotification({
message: '您没有访问此知识库的权限,请先申请权限',
type: 'warning',
})
);
return;
}
// 有权限则跳转到详情页
navigate(`/knowledge-base/${id}/datasets`);
};
const handleRequestAccess = (id, title) => {
setAccessRequestKnowledgeBase({
id,
title,
});
setShowAccessRequestModal(true);
};
const handleSubmitAccessRequest = async (requestData) => {
setIsSubmittingRequest(true);
try {
// 使用权限服务发送请求
await requestKnowledgeBaseAccess(requestData);
dispatch(
showNotification({
message: '权限申请已提交',
type: 'success',
})
);
// Close modal
setShowAccessRequestModal(false);
} catch (error) {
dispatch(
showNotification({
message: `权限申请失败: ${error.response?.data?.message || '请稍后重试'}`,
type: 'danger',
})
);
} finally {
setIsSubmittingRequest(false);
}
};
const handleDelete = (e, id) => {
e.preventDefault();
e.stopPropagation();
// Dispatch delete knowledge base action
dispatch(deleteKnowledgeBase(id))
.unwrap()
.then(() => {
dispatch(
showNotification({
message: '知识库已删除',
type: 'success',
})
);
})
.catch((error) => {
dispatch(
showNotification({
message: `删除失败: ${error.message || '未知错误'}`,
type: 'danger',
})
);
});
};
// Calculate total pages
const totalPages = Math.ceil(displayTotal / pagination.page_size);
// 打开创建知识库弹窗
const handleOpenCreateModal = () => {
// 默认知识库类型基于用户角色
let defaultType = 'private';
// 确保部门和组别字段使用当前用户的信息
setNewKnowledgeBase((prev) => ({
...prev,
department: currentUser?.department || '',
group: currentUser?.group || '',
type: defaultType,
}));
setShowCreateModal(true);
};
// 处理点击搜索结果
const handleSearchResultClick = (id, permissions) => {
if (permissions?.can_read) {
navigate(`/knowledge-base/${id}/datasets`);
}
};
return (
<div className='knowledge-base container my-4'>
<div className='api-mode-control mb-3'>
<ApiModeSwitch />
</div>
<div className='d-flex justify-content-between align-items-center mb-3'>
<SearchBar
searchKeyword={searchKeyword}
isSearching={isSearching}
onSearchChange={handleSearchInputChange}
onSearch={handleSearch}
onClearSearch={handleClearSearch}
placeholder='搜索知识库...'
searchResults={searchResults}
isSearchLoading={searchLoading}
onResultClick={handleSearchResultClick}
onRequestAccess={handleRequestAccess}
/>
<button className='btn btn-dark d-flex align-items-center gap-1' onClick={handleOpenCreateModal}>
<SvgIcon className={'plus'} />
新建知识库
</button>
</div>
{isSearching && (
<div className='alert alert-info'>
搜索结果: "{searchKeyword}" - 找到 {displayTotal} 个知识库
</div>
)}
{isLoading ? (
<div className='d-flex justify-content-center my-5'>
<div className='spinner-border' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
</div>
) : (
<>
<KnowledgeBaseList
knowledgeBases={displayData}
isSearching={isSearching}
onCardClick={handleCardClick}
onRequestAccess={handleRequestAccess}
onDelete={handleDelete}
/>
{/* Pagination */}
{totalPages > 1 && (
<Pagination
currentPage={pagination.page}
totalPages={totalPages}
pageSize={pagination.page_size}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
)}
</>
)}
{/* 新建知识库弹窗 */}
<CreateKnowledgeBaseModal
show={showCreateModal}
formData={newKnowledgeBase}
formErrors={formErrors}
isSubmitting={loading}
onClose={() => setShowCreateModal(false)}
onChange={handleInputChange}
onSubmit={handleCreateKnowledgeBase}
currentUser={currentUser}
/>
{/* 申请权限弹窗 */}
<AccessRequestModal
show={showAccessRequestModal}
knowledgeBaseId={accessRequestKnowledgeBase.id}
knowledgeBaseTitle={accessRequestKnowledgeBase.title}
onClose={() => setShowAccessRequestModal(false)}
onSubmit={handleSubmitAccessRequest}
isSubmitting={isSubmittingRequest}
/>
</div>
);
}