[dev]Update Knowledge base detail page

Add mock data
This commit is contained in:
susie-laptop 2025-03-07 18:05:33 -05:00
parent 6c1f0f3166
commit 4915514bde
15 changed files with 1356 additions and 365 deletions

15
package-lock.json generated
View File

@ -17,7 +17,8 @@
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
"redux-persist": "^6.0.0"
"redux-persist": "^6.0.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@ -4540,6 +4541,18 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",

View File

@ -19,7 +19,8 @@
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
"redux-persist": "^6.0.0"
"redux-persist": "^6.0.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",

View File

@ -270,7 +270,7 @@ export default function DatasetTab({ knowledgeBase }) {
<div className='d-flex justify-content-between align-items-center mb-3'>
<div className='d-flex gap-2'>
<button
className='btn btn-primary d-flex align-items-center gap-1'
className='btn btn-dark d-flex align-items-center gap-1'
onClick={() => setShowAddFileModal(true)}
>
<SvgIcon className='plus' />
@ -326,6 +326,34 @@ export default function DatasetTab({ knowledgeBase }) {
onSelectDocument={handleSelectDocument}
onDeleteDocument={handleDeleteDocument}
/>
{/* Pagination */}
<div className='d-flex justify-content-between align-items-center mt-3'>
<div>
每页行数:
<select className='form-select form-select d-inline-block ms-2' style={{ width: '70px' }}>
<option value='5'>5</option>
<option value='10'>10</option>
<option value='20'>20</option>
</select>
</div>
<div className='d-flex align-items-center'>
<span className='me-3'>1-5 of 10</span>
<nav aria-label='Page navigation'>
<ul className='pagination pagination mb-0'>
<li className='page-item'>
<button className='page-link' aria-label='Previous'>
<span aria-hidden='true'>&laquo;</span>
</button>
</li>
<li className='page-item'>
<button className='page-link' aria-label='Next'>
<span aria-hidden='true'>&raquo;</span>
</button>
</li>
</ul>
</nav>
</div>
</div>
{/* File upload modal */}
<FileUploadModal

View File

@ -1,10 +1,10 @@
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 SvgIcon from '../../components/SvgIcon';
import DatasetTab from './Detail/DatasetTab';
import SettingsTab from './Detail/SettingsTab';
import { showNotification } from '../../../store/notification.slice';
import SvgIcon from '../../../components/SvgIcon';
import DatasetTab from './DatasetTab';
import SettingsTab from './SettingsTab';
export default function KnowledgeBaseDetail() {
const { id, tab } = useParams();
@ -60,7 +60,9 @@ export default function KnowledgeBaseDetail() {
<div className='col-md-3 col-lg-2 border-end'>
<div className='py-4'>
<div className='h4 mb-3 text-center'>{knowledgeBase.name}</div>
<p className='text-center text-muted small mb-4'>{knowledgeBase.desc}</p>
<p className='text-center text-muted small mb-4'>
{knowledgeBase.desc || knowledgeBase.description || ''}
</p>
<hr />

View File

@ -8,6 +8,7 @@ import { updateKnowledgeBase, deleteKnowledgeBase } from '../../../store/knowled
import Breadcrumb from './components/Breadcrumb';
import KnowledgeBaseForm from './components/KnowledgeBaseForm';
import DeleteConfirmModal from './components/DeleteConfirmModal';
import UserPermissionsManager from './components/UserPermissionsManager';
export default function SettingsTab({ knowledgeBase }) {
const dispatch = useDispatch();
@ -16,114 +17,13 @@ export default function SettingsTab({ knowledgeBase }) {
// State for knowledge base form
const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({
name: knowledgeBase.name,
desc: knowledgeBase.desc,
desc: knowledgeBase.desc || knowledgeBase.description || '',
type: knowledgeBase.type || 'private', //
});
const [formErrors, setFormErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// State for pagination
const [currentPage, setCurrentPage] = useState(1);
const usersPerPage = 10;
// State for edit modal
const [showEditModal, setShowEditModal] = useState(false);
const [editUser, setEditUser] = useState(null);
// Mock data for users with permissions - convert to state so we can update it
const [users, setUsers] = useState([
{
id: '1001',
username: '张三',
email: 'zhang@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1002',
username: '李四',
email: 'li@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
{
id: '1003',
username: '王五',
email: 'wang@abc.com',
permissionType: '只读',
accessDuration: '三个月',
},
{
id: '1004',
username: '赵六',
email: 'zhao@abc.com',
permissionType: '完全访问',
accessDuration: '六个月',
},
{
id: '1005',
username: '钱七',
email: 'qian@abc.com',
permissionType: '只读',
accessDuration: '一周',
},
{
id: '1006',
username: '孙八',
email: 'sun@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1007',
username: '周九',
email: 'zhou@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
{
id: '1008',
username: '吴十',
email: 'wu@abc.com',
permissionType: '只读',
accessDuration: '三个月',
},
{
id: '1009',
username: '郑十一',
email: 'zheng@abc.com',
permissionType: '完全访问',
accessDuration: '六个月',
},
{
id: '1010',
username: '冯十二',
email: 'feng@abc.com',
permissionType: '只读',
accessDuration: '一周',
},
{
id: '1011',
username: '陈十三',
email: 'chen@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1012',
username: '褚十四',
email: 'chu@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
]);
// Get current users for pagination
const indexOfLastUser = currentPage * usersPerPage;
const indexOfFirstUser = indexOfLastUser - usersPerPage;
const currentUsers = users.slice(indexOfFirstUser, indexOfLastUser);
const totalPages = Math.ceil(users.length / usersPerPage);
// Handle knowledge base form input change
const handleInputChange = (e) => {
const { name, value } = e.target;
@ -153,6 +53,10 @@ export default function SettingsTab({ knowledgeBase }) {
errors.desc = '请输入知识库描述';
}
if (!knowledgeBaseForm.type) {
errors.type = '请选择知识库类型';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
@ -175,6 +79,8 @@ export default function SettingsTab({ knowledgeBase }) {
data: {
name: knowledgeBaseForm.name,
desc: knowledgeBaseForm.desc,
description: knowledgeBaseForm.desc, // Add description field for compatibility
type: knowledgeBaseForm.type,
},
})
)
@ -228,83 +134,6 @@ export default function SettingsTab({ knowledgeBase }) {
});
};
// Handle edit user permissions
const handleEditUser = (user) => {
setEditUser({ ...user });
setFormErrors({});
setShowEditModal(true);
};
// Handle input change in edit modal
const handleEditInputChange = (e) => {
const { name, value } = e.target;
setEditUser((prev) => ({
...prev,
[name]: value,
}));
// Clear error if exists
if (formErrors[name]) {
setFormErrors((prev) => ({
...prev,
[name]: '',
}));
}
};
// Validate edit form
const validateEditForm = () => {
const errors = {};
if (!editUser.permissionType) {
errors.permissionType = '请选择权限类型';
}
if (!editUser.accessDuration) {
errors.accessDuration = '请选择访问时长';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
// Handle save user permissions
const handleSaveUserPermissions = () => {
// Validate form
if (!validateEditForm()) {
return;
}
// Here you would typically call an API to update the user permissions
console.log('Updating user permissions:', editUser);
// Update the users array with the edited user
setUsers((prevUsers) => prevUsers.map((user) => (user.id === editUser.id ? { ...editUser } : user)));
// Show success notification (in a real app)
dispatch(showNotification({ message: '用户权限已更新', type: 'success' }));
// Close modal
setShowEditModal(false);
};
// Handle pagination
const handlePageChange = (pageNumber) => {
setCurrentPage(pageNumber);
};
// Handle delete user
const handleDeleteUser = (userId) => {
// Here you would typically call an API to delete the user
console.log('Deleting user:', userId);
// Update the users array by removing the deleted user
setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId));
// Show success notification (in a real app)
dispatch(showNotification({ message: '用户已删除', type: 'success' }));
};
return (
<>
{/* Breadcrumb navigation */}
@ -315,12 +144,14 @@ export default function SettingsTab({ knowledgeBase }) {
formData={knowledgeBaseForm}
formErrors={formErrors}
isSubmitting={isSubmitting}
knowledgeBase={knowledgeBase}
onInputChange={handleInputChange}
onSubmit={handleSubmit}
onDelete={() => setShowDeleteConfirm(true)}
/>
{/* User Permissions Manager */}
<UserPermissionsManager knowledgeBase={knowledgeBase} />
{/* Delete confirmation modal */}
<DeleteConfirmModal
show={showDeleteConfirm}
@ -329,143 +160,6 @@ export default function SettingsTab({ knowledgeBase }) {
onCancel={() => setShowDeleteConfirm(false)}
onConfirm={handleDelete}
/>
{/* Edit User Permissions Modal */}
{showEditModal && (
<div
className='modal-backdrop'
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1050,
}}
>
<div
className='modal-content bg-white rounded shadow'
style={{
width: '500px',
maxWidth: '90%',
padding: '20px',
}}
>
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
<h5 className='modal-title m-0'>编辑用户权限</h5>
<button
type='button'
className='btn-close'
onClick={() => setShowEditModal(false)}
aria-label='Close'
></button>
</div>
<div className='modal-body'>
<div className='mb-3'>
<label className='form-label'>用户信息</label>
<div className='card bg-light'>
<div className='card-body'>
<div className='d-flex justify-content-between mb-2'>
<span className='text-muted'>ID:</span>
<span>#{editUser?.id}</span>
</div>
<div className='d-flex justify-content-between mb-2'>
<span className='text-muted'>用户名:</span>
<span>{editUser?.username}</span>
</div>
<div className='d-flex justify-content-between'>
<span className='text-muted'>邮箱:</span>
<span>{editUser?.email}</span>
</div>
</div>
</div>
</div>
<div className='mb-4'>
<label className='form-label d-flex align-items-center'>
<SvgIcon className='key me-2' width='16' height='16' />
权限类型 <span className='text-danger'>*</span>
</label>
<div className='d-flex gap-2'>
<div
className={`p-3 rounded border bg-warning-subtle ${
editUser?.permissionType === '只读'
? 'border-warning'
: 'border-warning-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
handleEditInputChange({ target: { name: 'permissionType', value: '只读' } })
}
>
<div className='text-center text-warning fw-bold mb-1'>只读</div>
<div className='text-center text-muted small'>仅查看数据集内容</div>
</div>
<div
className={`p-3 rounded border bg-success-subtle ${
editUser?.permissionType === '完全访问'
? 'border-success'
: 'border-success-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
handleEditInputChange({
target: { name: 'permissionType', value: '完全访问' },
})
}
>
<div className='text-center text-success fw-bold mb-1'>完全访问</div>
<div className='text-center text-muted small'>查看编辑和管理数据</div>
</div>
</div>
{formErrors.permissionType && (
<div className='text-danger small mt-1'>{formErrors.permissionType}</div>
)}
</div>
<div className='mb-3'>
<label className='form-label d-flex align-items-center'>
<SvgIcon className='clock me-2' width='16' height='16' />
访问时长 <span className='text-danger'>*</span>
</label>
<select
className={`form-select ${formErrors.accessDuration ? 'is-invalid' : ''}`}
name='accessDuration'
value={editUser?.accessDuration || ''}
onChange={handleEditInputChange}
required
>
<option value=''>请选择访问时长</option>
<option value='一周'>一周</option>
<option value='一个月'>一个月</option>
<option value='三个月'>三个月</option>
<option value='六个月'>六个月</option>
<option value='永久'>永久</option>
</select>
{formErrors.accessDuration && (
<div className='invalid-feedback'>{formErrors.accessDuration}</div>
)}
</div>
</div>
<div className='modal-footer d-flex justify-content-end gap-2'>
<button
type='button'
className='btn btn-outline-secondary'
onClick={() => setShowEditModal(false)}
>
取消
</button>
<button type='button' className='btn btn-dark' onClick={handleSaveUserPermissions}>
保存
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -10,7 +10,7 @@ const Breadcrumb = ({ knowledgeBase, activeTab }) => {
<nav aria-label='breadcrumb'>
<ol className='breadcrumb mb-0'>
<li className='breadcrumb-item'>
<Link className='text-secondary text-decoration-none' to='/knowledge-base'>
<Link className='text-secondary text-decoration-none' to='/'>
知识库
</Link>
</li>

View File

@ -7,7 +7,6 @@ const KnowledgeBaseForm = ({
formData,
formErrors,
isSubmitting,
knowledgeBase,
onInputChange,
onSubmit,
onDelete,
@ -54,10 +53,11 @@ const KnowledgeBaseForm = ({
<input
className='form-check-input'
type='radio'
name='knowledgeType'
name='type'
id='typePrivate'
checked={knowledgeBase.type === 'private'}
readOnly
value='private'
checked={formData.type === 'private'}
onChange={onInputChange}
/>
<label className='form-check-label' htmlFor='typePrivate'>
私有知识库
@ -67,27 +67,17 @@ const KnowledgeBaseForm = ({
<input
className='form-check-input'
type='radio'
name='knowledgeType'
name='type'
id='typePublic'
checked={knowledgeBase.type === 'public'}
readOnly
value='public'
checked={formData.type === 'public'}
onChange={onInputChange}
/>
<label className='form-check-label' htmlFor='typePublic'>
公共知识库
</label>
</div>
</div>
<div className='mb-3'>
<label className='form-label'>创建时间</label>
<p className='form-control-static'>{new Date(knowledgeBase.create_time).toLocaleString()}</p>
</div>
<div className='mb-3'>
<label className='form-label'>最后更新时间</label>
<p className='form-control-static'>{new Date(knowledgeBase.update_time).toLocaleString()}</p>
</div>
<div className='d-flex justify-content-between'>
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
{isSubmitting ? (

View File

@ -0,0 +1,789 @@
import React, { useState, useRef, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { showNotification } from '../../../../store/notification.slice';
import SvgIcon from '../../../../components/SvgIcon';
/**
* 用户权限管理组件
*/
const UserPermissionsManager = ({ knowledgeBase }) => {
const dispatch = useDispatch();
// State for pagination
const [currentPage, setCurrentPage] = useState(1);
const usersPerPage = 10;
// State for edit modal
const [showEditModal, setShowEditModal] = useState(false);
const [editUser, setEditUser] = useState(null);
// State for add user modal
const [showAddUserModal, setShowAddUserModal] = useState(false);
const [newUser, setNewUser] = useState({
username: '',
email: '',
permissionType: '只读',
accessDuration: '一个月',
});
// State for batch operations
const [selectedUsers, setSelectedUsers] = useState([]);
const [selectAll, setSelectAll] = useState(false);
const [showBatchDropdown, setShowBatchDropdown] = useState(false);
const batchDropdownRef = useRef(null);
// State for batch edit modal
const [showBatchEditModal, setShowBatchEditModal] = useState(false);
const [batchEditData, setBatchEditData] = useState({
permissionType: '只读',
accessDuration: '一个月',
});
// Form errors
const [formErrors, setFormErrors] = useState({});
// Mock data for users with permissions
const [users, setUsers] = useState([
{
id: '1001',
username: '张三',
email: 'zhang@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1002',
username: '李四',
email: 'li@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
{
id: '1003',
username: '王五',
email: 'wang@abc.com',
permissionType: '只读',
accessDuration: '三个月',
},
{
id: '1004',
username: '赵六',
email: 'zhao@abc.com',
permissionType: '完全访问',
accessDuration: '六个月',
},
{
id: '1005',
username: '钱七',
email: 'qian@abc.com',
permissionType: '只读',
accessDuration: '一周',
},
{
id: '1006',
username: '孙八',
email: 'sun@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1007',
username: '周九',
email: 'zhou@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
{
id: '1008',
username: '吴十',
email: 'wu@abc.com',
permissionType: '只读',
accessDuration: '三个月',
},
]);
// Get current users for pagination
const indexOfLastUser = currentPage * usersPerPage;
const indexOfFirstUser = indexOfLastUser - usersPerPage;
const currentUsers = users.slice(indexOfFirstUser, indexOfLastUser);
const totalPages = Math.ceil(users.length / usersPerPage);
// Handle click outside batch dropdown
useEffect(() => {
function handleClickOutside(event) {
if (batchDropdownRef.current && !batchDropdownRef.current.contains(event.target)) {
setShowBatchDropdown(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Handle edit user permissions
const handleEditUser = (user) => {
setEditUser({ ...user });
setFormErrors({});
setShowEditModal(true);
};
// Handle input change in edit modal
const handleEditInputChange = (e) => {
const { name, value } = e.target;
setEditUser((prev) => ({
...prev,
[name]: value,
}));
// Clear error if exists
if (formErrors[name]) {
setFormErrors((prev) => ({
...prev,
[name]: '',
}));
}
};
// Handle input change in add user modal
const handleAddUserInputChange = (e) => {
const { name, value } = e.target;
setNewUser((prev) => ({
...prev,
[name]: value,
}));
// Clear error if exists
if (formErrors[name]) {
setFormErrors((prev) => ({
...prev,
[name]: '',
}));
}
};
// Handle input change in batch edit modal
const handleBatchEditInputChange = (e) => {
const { name, value } = e.target;
setBatchEditData((prev) => ({
...prev,
[name]: value,
}));
};
// Validate edit form
const validateEditForm = () => {
const errors = {};
if (!editUser.username.trim()) {
errors.username = '请输入用户名';
}
if (!editUser.email.trim()) {
errors.email = '请输入邮箱';
} else if (!/\S+@\S+\.\S+/.test(editUser.email)) {
errors.email = '请输入有效的邮箱地址';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
// Validate add user form
const validateAddUserForm = () => {
const errors = {};
if (!newUser.username.trim()) {
errors.username = '请输入用户名';
}
if (!newUser.email.trim()) {
errors.email = '请输入邮箱';
} else if (!/\S+@\S+\.\S+/.test(newUser.email)) {
errors.email = '请输入有效的邮箱地址';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
// Handle save user permissions
const handleSaveUserPermissions = () => {
// Validate form
if (!validateEditForm()) {
return;
}
// Here you would typically call an API to update the user permissions
console.log('Updating user permissions:', editUser);
// Update the users array with the edited user
setUsers((prevUsers) => prevUsers.map((user) => (user.id === editUser.id ? { ...editUser } : user)));
// Show success notification
dispatch(showNotification({ message: '用户权限已更新', type: 'success' }));
// Close modal
setShowEditModal(false);
};
// Handle add new user
const handleAddUser = () => {
// Validate form
if (!validateAddUserForm()) {
return;
}
// Generate a new ID
const newId = `user-${Date.now()}`;
// Create new user object
const userToAdd = {
id: newId,
username: newUser.username,
email: newUser.email,
permissionType: newUser.permissionType,
accessDuration: newUser.accessDuration,
};
// Add new user to the users array
setUsers((prevUsers) => [...prevUsers, userToAdd]);
// Show success notification
dispatch(showNotification({ message: '用户已添加', type: 'success' }));
// Reset form and close modal
setNewUser({
username: '',
email: '',
permissionType: '只读',
accessDuration: '一个月',
});
setShowAddUserModal(false);
};
// Handle delete user
const handleDeleteUser = (userId) => {
// Here you would typically call an API to delete the user
console.log('Deleting user:', userId);
// Remove user from the users array
setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId));
// Remove from selected users if present
setSelectedUsers((prev) => prev.filter((id) => id !== userId));
// Show success notification
dispatch(showNotification({ message: '用户已删除', type: 'success' }));
};
// Handle batch delete
const handleBatchDelete = () => {
if (selectedUsers.length === 0) return;
// Here you would typically call an API to delete the selected users
console.log('Batch deleting users:', selectedUsers);
// Remove selected users from the users array
setUsers((prevUsers) => prevUsers.filter((user) => !selectedUsers.includes(user.id)));
// Reset selection
setSelectedUsers([]);
setSelectAll(false);
setShowBatchDropdown(false);
// Show success notification
dispatch(showNotification({ message: `已删除 ${selectedUsers.length} 个用户`, type: 'success' }));
};
// Handle batch edit
const handleBatchEdit = () => {
if (selectedUsers.length === 0) return;
// Here you would typically call an API to update the selected users
console.log('Batch editing users:', selectedUsers, 'with data:', batchEditData);
// Update selected users in the users array
setUsers((prevUsers) =>
prevUsers.map((user) => (selectedUsers.includes(user.id) ? { ...user, ...batchEditData } : user))
);
// Close modal
setShowBatchEditModal(false);
setShowBatchDropdown(false);
// Show success notification
dispatch(showNotification({ message: `已更新 ${selectedUsers.length} 个用户的权限`, type: 'success' }));
};
// Handle select all checkbox
const handleSelectAll = () => {
if (selectAll) {
setSelectedUsers([]);
} else {
setSelectedUsers(currentUsers.map((user) => user.id));
}
setSelectAll(!selectAll);
};
// Handle individual user selection
const handleSelectUser = (userId) => {
if (selectedUsers.includes(userId)) {
setSelectedUsers(selectedUsers.filter((id) => id !== userId));
setSelectAll(false);
} else {
setSelectedUsers([...selectedUsers, userId]);
// Check if all current page users are now selected
if (selectedUsers.length + 1 === currentUsers.length) {
setSelectAll(true);
}
}
};
// Handle page change
const handlePageChange = (pageNumber) => {
setCurrentPage(pageNumber);
// Reset selection when changing pages
setSelectedUsers([]);
setSelectAll(false);
};
return (
<div className='card border-0 shadow-sm mt-4'>
<div className='card-body'>
<div className='d-flex justify-content-between align-items-center mb-4'>
<h5 className='card-title mb-0'>用户权限管理</h5>
<div className='d-flex gap-2'>
{selectedUsers.length > 0 && (
<div className='dropdown' ref={batchDropdownRef}>
<button
className='btn btn-outline-primary btn-sm dropdown-toggle'
onClick={() => setShowBatchDropdown(!showBatchDropdown)}
>
批量操作 ({selectedUsers.length})
</button>
{showBatchDropdown && (
<ul className='dropdown-menu show'>
<li>
<button
className='dropdown-item'
onClick={() => {
setShowBatchEditModal(true);
setShowBatchDropdown(false);
}}
>
<SvgIcon className='edit' />
<span className='ms-2'>批量修改权限</span>
</button>
</li>
<li>
<button className='dropdown-item text-danger' onClick={handleBatchDelete}>
<SvgIcon className='trash' />
<span className='ms-2'>批量删除</span>
</button>
</li>
</ul>
)}
</div>
)}
<button className='btn btn-dark btn-sm' onClick={() => setShowAddUserModal(true)}>
<SvgIcon className='plus' />
添加用户
</button>
</div>
</div>
<div className='table-responsive'>
<table className='table table-hover'>
<thead>
<tr>
<th>
<div className='form-check'>
<input
className='form-check-input'
type='checkbox'
checked={selectAll}
onChange={handleSelectAll}
/>
</div>
</th>
<th>用户名</th>
<th>邮箱</th>
<th>权限类型</th>
<th>访问期限</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{currentUsers.length > 0 ? (
currentUsers.map((user) => (
<tr key={user.id}>
<td>
<div className='form-check'>
<input
className='form-check-input'
type='checkbox'
checked={selectedUsers.includes(user.id)}
onChange={() => handleSelectUser(user.id)}
/>
</div>
</td>
<td>{user.username}</td>
<td>{user.email}</td>
<td>
<span
className={`badge ${
user.permissionType === '完全访问'
? 'bg-success-subtle text-success'
: 'bg-warning-subtle text-warning'
}`}
>
{user.permissionType}
</span>
</td>
<td>{user.accessDuration}</td>
<td>
<div className='btn-group'>
<button
className='btn btn-sm text-primary'
onClick={() => handleEditUser(user)}
>
<SvgIcon className='edit' />
</button>
<button
className='btn btn-sm text-danger'
onClick={() => handleDeleteUser(user.id)}
>
<SvgIcon className='trash' />
</button>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan='6' className='text-center'>
暂无用户
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<nav aria-label='Page navigation'>
<ul className='pagination justify-content-center'>
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
<button
className='page-link'
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
上一页
</button>
</li>
{[...Array(totalPages)].map((_, index) => (
<li key={index} className={`page-item ${currentPage === index + 1 ? 'active' : ''}`}>
<button className='page-link' onClick={() => handlePageChange(index + 1)}>
{index + 1}
</button>
</li>
))}
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
<button
className='page-link'
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
下一页
</button>
</li>
</ul>
</nav>
)}
{/* Edit User Modal */}
{showEditModal && (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title'>编辑用户权限</h5>
<button
type='button'
className='btn-close'
onClick={() => setShowEditModal(false)}
></button>
</div>
<div className='modal-body'>
<form>
<div className='mb-3'>
<label htmlFor='username' className='form-label'>
用户名
</label>
<input
type='text'
className={`form-control ${formErrors.username ? 'is-invalid' : ''}`}
id='username'
name='username'
value={editUser.username}
onChange={handleEditInputChange}
/>
{formErrors.username && (
<div className='invalid-feedback'>{formErrors.username}</div>
)}
</div>
<div className='mb-3'>
<label htmlFor='email' className='form-label'>
邮箱
</label>
<input
type='email'
className={`form-control ${formErrors.email ? 'is-invalid' : ''}`}
id='email'
name='email'
value={editUser.email}
onChange={handleEditInputChange}
/>
{formErrors.email && (
<div className='invalid-feedback'>{formErrors.email}</div>
)}
</div>
<div className='mb-3'>
<label htmlFor='permissionType' className='form-label'>
权限类型
</label>
<select
className='form-select'
id='permissionType'
name='permissionType'
value={editUser.permissionType}
onChange={handleEditInputChange}
>
<option value='只读'>只读</option>
<option value='完全访问'>完全访问</option>
</select>
</div>
<div className='mb-3'>
<label htmlFor='accessDuration' className='form-label'>
访问期限
</label>
<select
className='form-select'
id='accessDuration'
name='accessDuration'
value={editUser.accessDuration}
onChange={handleEditInputChange}
>
<option value='一周'>一周</option>
<option value='一个月'>一个月</option>
<option value='三个月'>三个月</option>
<option value='六个月'>六个月</option>
<option value='永久'>永久</option>
</select>
</div>
</form>
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-secondary'
onClick={() => setShowEditModal(false)}
>
取消
</button>
<button
type='button'
className='btn btn-primary'
onClick={handleSaveUserPermissions}
>
保存
</button>
</div>
</div>
</div>
</div>
)}
{/* Add User Modal */}
{showAddUserModal && (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title'>添加用户</h5>
<button
type='button'
className='btn-close'
onClick={() => setShowAddUserModal(false)}
></button>
</div>
<div className='modal-body'>
<form>
<div className='mb-3'>
<label htmlFor='newUsername' className='form-label'>
用户名
</label>
<input
type='text'
className={`form-control ${formErrors.username ? 'is-invalid' : ''}`}
id='newUsername'
name='username'
value={newUser.username}
onChange={handleAddUserInputChange}
/>
{formErrors.username && (
<div className='invalid-feedback'>{formErrors.username}</div>
)}
</div>
<div className='mb-3'>
<label htmlFor='newEmail' className='form-label'>
邮箱
</label>
<input
type='email'
className={`form-control ${formErrors.email ? 'is-invalid' : ''}`}
id='newEmail'
name='email'
value={newUser.email}
onChange={handleAddUserInputChange}
/>
{formErrors.email && (
<div className='invalid-feedback'>{formErrors.email}</div>
)}
</div>
<div className='mb-3'>
<label htmlFor='newPermissionType' className='form-label'>
权限类型
</label>
<select
className='form-select'
id='newPermissionType'
name='permissionType'
value={newUser.permissionType}
onChange={handleAddUserInputChange}
>
<option value='只读'>只读</option>
<option value='完全访问'>完全访问</option>
</select>
</div>
<div className='mb-3'>
<label htmlFor='newAccessDuration' className='form-label'>
访问期限
</label>
<select
className='form-select'
id='newAccessDuration'
name='accessDuration'
value={newUser.accessDuration}
onChange={handleAddUserInputChange}
>
<option value='一周'>一周</option>
<option value='一个月'>一个月</option>
<option value='三个月'>三个月</option>
<option value='六个月'>六个月</option>
<option value='永久'>永久</option>
</select>
</div>
</form>
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-secondary'
onClick={() => setShowAddUserModal(false)}
>
取消
</button>
<button type='button' className='btn btn-primary' onClick={handleAddUser}>
添加
</button>
</div>
</div>
</div>
</div>
)}
{/* Batch Edit Modal */}
{showBatchEditModal && (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title'>批量修改权限</h5>
<button
type='button'
className='btn-close'
onClick={() => setShowBatchEditModal(false)}
></button>
</div>
<div className='modal-body'>
<p>
您正在修改 <strong>{selectedUsers.length}</strong> 个用户的权限
</p>
<form>
<div className='mb-3'>
<label htmlFor='batchPermissionType' className='form-label'>
权限类型
</label>
<select
className='form-select'
id='batchPermissionType'
name='permissionType'
value={batchEditData.permissionType}
onChange={handleBatchEditInputChange}
>
<option value='只读'>只读</option>
<option value='完全访问'>完全访问</option>
</select>
</div>
<div className='mb-3'>
<label htmlFor='batchAccessDuration' className='form-label'>
访问期限
</label>
<select
className='form-select'
id='batchAccessDuration'
name='accessDuration'
value={batchEditData.accessDuration}
onChange={handleBatchEditInputChange}
>
<option value='一周'>一周</option>
<option value='一个月'>一个月</option>
<option value='三个月'>三个月</option>
<option value='六个月'>六个月</option>
<option value='永久'>永久</option>
</select>
</div>
</form>
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-secondary'
onClick={() => setShowBatchEditModal(false)}
>
取消
</button>
<button type='button' className='btn btn-primary' onClick={handleBatchEdit}>
保存
</button>
</div>
</div>
</div>
</div>
)}
{/* Modal backdrop */}
{(showEditModal || showAddUserModal || showBatchEditModal) && (
<div className='modal-backdrop fade show'></div>
)}
</div>
</div>
);
};
export default UserPermissionsManager;

View File

@ -246,6 +246,7 @@ export default function KnowledgeBase() {
createKnowledgeBase({
name: newKnowledgeBase.name,
desc: newKnowledgeBase.desc,
description: newKnowledgeBase.desc,
type: 'private', // Default type
})
);
@ -295,11 +296,17 @@ export default function KnowledgeBase() {
setShowAccessRequestModal(false);
};
const handleDelete = (e, id) => {
e.preventDefault();
e.stopPropagation();
console.log(id);
};
// Calculate total pages
const totalPages = Math.ceil(displayTotal / pagination.page_size);
return (
<div className='knowledge-base container mt-4'>
<div className='knowledge-base container my-4'>
<div className='d-flex justify-content-between align-items-center mb-3'>
<SearchBar
searchKeyword={searchKeyword}
@ -336,6 +343,7 @@ export default function KnowledgeBase() {
isSearching={isSearching}
onCardClick={handleCardClick}
onRequestAccess={handleRequestAccess}
onDelete={handleDelete}
/>
{/* Pagination */}
@ -401,8 +409,47 @@ export default function KnowledgeBase() {
<label className='form-label'>知识库名称</label>
<input type='text' className='form-control' value={accessRequestData.title} readOnly />
</div>
{/* <div className='mb-4'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='key' />
访问级别 <span className='text-danger'>*</span>
</label>
<div className='d-flex gap-2'>
<div
className={`p-3 rounded border bg-warning-subtle ${
accessRequestData.accessType === '只读访问'
? 'border-warning bg-warning-subtle'
: 'border-warning-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' }))
}
>
<div className='text-center text-warning fw-bold mb-1'>只读访问</div>
<div className='text-center text-muted small'>仅查看数据集内容</div>
</div>
<div
className={`p-3 rounded border bg-success-subtle ${
accessRequestData.accessType === '完全访问'
? 'border-success'
: 'border-success-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' }))
}
>
<div className='text-center text-success fw-bold mb-1'>完全访问</div>
<div className='text-center text-muted small'>查看编辑和管理数据</div>
</div>
</div>
</div> */}
<div className='mb-3'>
<label className='form-label'>权限类型</label>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='key' />
访问级别 <span className='text-danger'>*</span>
</label>
<select
className='form-select'
name='accessType'
@ -413,8 +460,12 @@ export default function KnowledgeBase() {
<option value='编辑权限'>编辑权限</option>
</select>
</div>
<div className='mb-3'>
<label className='form-label'>访问时长</label>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='calendar' />
访问时长 <span className='text-danger'>*</span>
</label>
<select
className='form-select'
name='duration'
@ -429,7 +480,8 @@ export default function KnowledgeBase() {
</select>
</div>
<div className='mb-3'>
<label htmlFor='projectInfo' className='form-label'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='clipboard' />
项目信息 <span className='text-danger'>*</span>
</label>
<input
@ -446,7 +498,8 @@ export default function KnowledgeBase() {
)}
</div>
<div className='mb-3'>
<label htmlFor='reason' className='form-label'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='chat' />
申请原因 <span className='text-danger'>*</span>
</label>
<textarea
@ -463,7 +516,7 @@ export default function KnowledgeBase() {
)}
</div>
</div>
<div className='modal-footer'>
<div className='modal-footer gap-2'>
<button
type='button'
className='btn btn-secondary'
@ -471,7 +524,7 @@ export default function KnowledgeBase() {
>
取消
</button>
<button type='button' className='btn btn-primary' onClick={handleSubmitAccessRequest}>
<button type='button' className='btn btn-dark' onClick={handleSubmitAccessRequest}>
提交申请
</button>
</div>

View File

@ -65,11 +65,11 @@ const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, on
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
</div>
</div>
<div className='modal-footer'>
<div className='modal-footer gap-2'>
<button type='button' className='btn btn-secondary' onClick={onClose}>
取消
</button>
<button type='button' className='btn btn-primary' onClick={onSubmit} disabled={isSubmitting}>
<button type='button' className='btn btn-dark' onClick={onSubmit} disabled={isSubmitting}>
{isSubmitting ? (
<>
<span

View File

@ -1,10 +1,10 @@
import React from 'react';
import KnowledgeCard from '../KnowledgeCard';
import KnowledgeCard from './KnowledgeCard';
/**
* 知识库列表组件
*/
const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequestAccess }) => {
const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequestAccess, onDelete }) => {
if (knowledgeBases.length === 0) {
return (
<div className='alert alert-warning'>
@ -20,12 +20,13 @@ const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequest
<KnowledgeCard
id={item.id}
title={item.name}
description={item.desc}
description={item.description || item.desc || ''}
documents={item.document_count || 0}
date={new Date(item.create_time).toLocaleDateString()}
access={item.permissions.can_edit ? 'full' : item.permissions.can_read ? 'read' : 'none'}
date={new Date(item.create_time || item.created_at).toLocaleDateString()}
access={item.permissions?.can_edit ? 'full' : item.permissions?.can_read ? 'read' : 'none'}
onClick={() => onCardClick(item.id)}
onRequestAccess={onRequestAccess}
onDelete={(e) => onDelete(e, item.id)}
/>
</React.Fragment>
))}

View File

@ -1,8 +1,18 @@
import React from 'react';
import SvgIcon from '../../components/SvgIcon';
import SvgIcon from '../../../components/SvgIcon';
import { useNavigate } from 'react-router-dom';
export default function KnowledgeCard({ id, title, description, documents, date, access, onClick, onRequestAccess }) {
export default function KnowledgeCard({
id,
title,
description,
documents,
date,
access,
onClick,
onRequestAccess,
onDelete,
}) {
const navigate = useNavigate();
const handleNewChat = (e) => {
@ -17,6 +27,16 @@ export default function KnowledgeCard({ id, title, description, documents, date,
onRequestAccess(id, title);
};
//
const descriptionStyle = {
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
minHeight: '3rem', // 使
};
return (
<div className='knowledge-card card shadow border-0 p-0 col' onClick={onClick}>
<div className='card-body'>
@ -26,13 +46,15 @@ export default function KnowledgeCard({ id, title, description, documents, date,
<SvgIcon className={'more-dot'} />
</button>
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
<li className='p-1 hoverdown-item px-2'>
<li className='p-1 hoverdown-item px-2' onClick={onDelete}>
删除
<SvgIcon className={'trash'} />
</li>
</ul>
</div>
<p className='card-text text-muted'>{description}</p>
<p className='card-text text-muted mb-3' style={descriptionStyle} title={description}>
{description}
</p>
<div className='text-muted d-flex align-items-center gap-1'>
<SvgIcon className={'file'} />
{documents} 文档

View File

@ -2,7 +2,7 @@ import React, { Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import Mainlayout from '../layouts/Mainlayout';
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
import KnowledgeBaseDetail from '../pages/KnowledgeBase/KnowledgeBaseDetail';
import KnowledgeBaseDetail from '../pages/KnowledgeBase/Detail/KnowledgeBaseDetail';
import Chat from '../pages/Chat/Chat';
import Loading from '../components/Loading';
import Login from '../pages/Auth/Login';

View File

@ -1,8 +1,12 @@
import axios from 'axios';
import CryptoJS from 'crypto-js';
import { mockGet, mockPost, mockPut, mockDel } from './mockApi';
const secretKey = import.meta.env.VITE_SECRETKEY;
// Flag to enable/disable mock API
const USE_MOCK_API = true; // Set to false to use real API
// Create Axios instance with base URL
const api = axios.create({
baseURL: '/api',
@ -58,12 +62,30 @@ api.interceptors.response.use(
// Define common HTTP methods
const get = async (url, params = {}) => {
if (USE_MOCK_API) {
try {
const response = await mockGet(url, params);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const res = await api.get(url, { params });
return res.data;
};
// Handle POST requests for JSON data
const post = async (url, data, isMultipart = false) => {
if (USE_MOCK_API) {
try {
const response = await mockPost(url, data);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const headers = isMultipart
? { 'Content-Type': 'multipart/form-data' } // For file uploads
: { 'Content-Type': 'application/json' }; // For JSON data
@ -74,6 +96,15 @@ const post = async (url, data, isMultipart = false) => {
// Handle PUT requests
const put = async (url, data) => {
if (USE_MOCK_API) {
try {
const response = await mockPut(url, data);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const res = await api.put(url, data, {
headers: { 'Content-Type': 'application/json' },
});
@ -82,11 +113,25 @@ const put = async (url, data) => {
// Handle DELETE requests
const del = async (url) => {
if (USE_MOCK_API) {
try {
const response = await mockDel(url);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const res = await api.delete(url);
return res.data;
};
const upload = async (url, data) => {
if (USE_MOCK_API) {
console.warn('[MOCK API] Upload functionality is not mocked');
return { success: true, message: 'Mock upload successful' };
}
const axiosInstance = await axios.create({
baseURL: '/api',
headers: {
@ -96,4 +141,5 @@ const upload = async (url, data) => {
const res = await axiosInstance.post(url, data);
return res.data;
};
export { get, post, put, del, upload };

352
src/services/mockApi.js Normal file
View File

@ -0,0 +1,352 @@
// Mock API service for development without backend
import { v4 as uuidv4 } from 'uuid';
// Mock data for knowledge bases
const mockKnowledgeBases = [
{
id: 'kb-001',
name: 'Frontend Development',
description: 'Resources and guides for frontend development including React, Vue, and Angular',
created_at: '2023-10-15T08:30:00Z',
updated_at: '2023-12-20T14:45:00Z',
create_time: '2023-10-15T08:30:00Z',
update_time: '2023-12-20T14:45:00Z',
type: 'private',
owner: {
id: 'user-001',
username: 'johndoe',
email: 'john@example.com',
},
document_count: 15,
tags: ['react', 'javascript', 'frontend'],
permissions: {
can_edit: true,
can_read: true,
},
documents: [
{
id: 'doc-001',
name: 'React Best Practices.pdf',
description: 'A guide to React best practices and patterns',
size: '1.2MB',
create_time: '2023-10-20T09:15:00Z',
update_time: '2023-10-20T09:15:00Z',
},
{
id: 'doc-002',
name: 'Vue.js Tutorial.docx',
description: 'Step-by-step tutorial for Vue.js beginners',
size: '850KB',
create_time: '2023-11-05T14:30:00Z',
update_time: '2023-11-05T14:30:00Z',
},
{
id: 'doc-003',
name: 'JavaScript ES6 Features.pdf',
description: 'Overview of ES6 features and examples',
size: '1.5MB',
create_time: '2023-11-15T11:45:00Z',
update_time: '2023-11-15T11:45:00Z',
},
],
},
{
id: 'kb-002',
name: 'Backend Technologies',
description: 'Information about backend frameworks, databases, and server configurations',
created_at: '2023-09-05T10:15:00Z',
updated_at: '2024-01-10T09:20:00Z',
create_time: '2023-09-05T10:15:00Z',
update_time: '2024-01-10T09:20:00Z',
type: 'private',
owner: {
id: 'user-001',
username: 'johndoe',
email: 'john@example.com',
},
document_count: 23,
tags: ['nodejs', 'python', 'databases'],
permissions: {
can_edit: true,
can_read: true,
},
},
{
id: 'kb-003',
name: 'DevOps Practices',
description: 'Best practices for CI/CD, containerization, and cloud deployment',
created_at: '2023-11-12T15:45:00Z',
updated_at: '2024-02-05T11:30:00Z',
create_time: '2023-11-12T15:45:00Z',
update_time: '2024-02-05T11:30:00Z',
type: 'public',
owner: {
id: 'user-002',
username: 'janedoe',
email: 'jane@example.com',
},
document_count: 18,
tags: ['docker', 'kubernetes', 'aws'],
permissions: {
can_edit: false,
can_read: true,
},
},
{
id: 'kb-004',
name: 'Machine Learning Fundamentals',
description: 'Introduction to machine learning concepts, algorithms, and frameworks',
created_at: '2023-08-20T09:00:00Z',
updated_at: '2024-01-25T16:15:00Z',
create_time: '2023-08-20T09:00:00Z',
update_time: '2024-01-25T16:15:00Z',
type: 'public',
owner: {
id: 'user-003',
username: 'alexsmith',
email: 'alex@example.com',
},
document_count: 30,
tags: ['ml', 'python', 'tensorflow'],
permissions: {
can_edit: false,
can_read: true,
},
},
{
id: 'kb-005',
name: 'UI/UX Design Principles',
description: 'Guidelines for creating effective and user-friendly interfaces',
created_at: '2023-12-01T13:20:00Z',
updated_at: '2024-02-15T10:45:00Z',
create_time: '2023-12-01T13:20:00Z',
update_time: '2024-02-15T10:45:00Z',
type: 'private',
owner: {
id: 'user-002',
username: 'janedoe',
email: 'jane@example.com',
},
document_count: 12,
tags: ['design', 'ui', 'ux'],
permissions: {
can_edit: true,
can_read: true,
},
},
{
id: 'kb-006',
name: 'Mobile App Development',
description: 'Resources for iOS, Android, and cross-platform mobile development',
created_at: '2023-10-25T11:10:00Z',
updated_at: '2024-01-30T14:00:00Z',
create_time: '2023-10-25T11:10:00Z',
update_time: '2024-01-30T14:00:00Z',
type: 'private',
owner: {
id: 'user-001',
username: 'johndoe',
email: 'john@example.com',
},
document_count: 20,
tags: ['mobile', 'react-native', 'flutter'],
permissions: {
can_edit: true,
can_read: true,
},
},
{
id: 'kb-007',
name: 'Cybersecurity Best Practices',
description: 'Guidelines for securing applications, networks, and data',
created_at: '2023-09-18T14:30:00Z',
updated_at: '2024-02-10T09:15:00Z',
create_time: '2023-09-18T14:30:00Z',
update_time: '2024-02-10T09:15:00Z',
type: 'private',
owner: {
id: 'user-004',
username: 'sarahwilson',
email: 'sarah@example.com',
},
document_count: 25,
tags: ['security', 'encryption', 'authentication'],
permissions: {
can_edit: false,
can_read: false,
},
},
{
id: 'kb-008',
name: 'Data Science Toolkit',
description: 'Tools and techniques for data analysis, visualization, and modeling',
created_at: '2023-11-05T10:00:00Z',
updated_at: '2024-01-20T15:30:00Z',
create_time: '2023-11-05T10:00:00Z',
update_time: '2024-01-20T15:30:00Z',
type: 'public',
owner: {
id: 'user-003',
username: 'alexsmith',
email: 'alex@example.com',
},
document_count: 28,
tags: ['data-science', 'python', 'visualization'],
permissions: {
can_edit: false,
can_read: true,
},
},
];
// In-memory store for CRUD operations
let knowledgeBases = [...mockKnowledgeBases];
// Helper function for pagination
const paginate = (array, page_size, page) => {
const startIndex = (page - 1) * page_size;
const endIndex = startIndex + page_size;
const items = array.slice(startIndex, endIndex);
return {
items,
total: array.length,
page,
page_size,
};
};
// Mock API functions
export const mockGet = async (url, params = {}) => {
console.log(`[MOCK API] GET ${url}`, params);
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500));
// Knowledge bases list with pagination
if (url === '/knowledge-bases/') {
const { page = 1, page_size = 10 } = params;
return paginate(knowledgeBases, page_size, page);
}
// Knowledge base search
if (url === '/knowledge-bases/search/') {
const { keyword = '', page = 1, page_size = 10 } = params;
const filtered = knowledgeBases.filter(
(kb) =>
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
kb.description.toLowerCase().includes(keyword.toLowerCase()) ||
kb.tags.some((tag) => tag.toLowerCase().includes(keyword.toLowerCase()))
);
return paginate(filtered, page_size, page);
}
// Knowledge base details
if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
const id = url.split('/')[2];
const knowledgeBase = knowledgeBases.find((kb) => kb.id === id);
if (!knowledgeBase) {
throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
}
return knowledgeBase;
}
throw { response: { status: 404, data: { message: 'Not found' } } };
};
export const mockPost = async (url, data) => {
console.log(`[MOCK API] POST ${url}`, data);
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 700));
// Create knowledge base
if (url === '/knowledge-bases/') {
const newKnowledgeBase = {
id: `kb-${uuidv4().slice(0, 8)}`,
name: data.name,
description: data.description || '',
desc: data.desc || data.description || '',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
create_time: new Date().toISOString(),
update_time: new Date().toISOString(),
type: data.type || 'private',
owner: {
id: 'user-001',
username: 'johndoe',
email: 'john@example.com',
},
document_count: 0,
tags: data.tags || [],
permissions: {
can_edit: true,
can_read: true,
},
documents: [],
};
knowledgeBases.push(newKnowledgeBase);
return newKnowledgeBase;
}
throw { response: { status: 404, data: { message: 'Not found' } } };
};
export const mockPut = async (url, data) => {
console.log(`[MOCK API] PUT ${url}`, data);
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 600));
// Update knowledge base
if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
const id = url.split('/')[2];
const index = knowledgeBases.findIndex((kb) => kb.id === id);
if (index === -1) {
throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
}
const updatedKnowledgeBase = {
...knowledgeBases[index],
...data,
updated_at: new Date().toISOString(),
update_time: new Date().toISOString(),
};
knowledgeBases[index] = updatedKnowledgeBase;
return updatedKnowledgeBase;
}
throw { response: { status: 404, data: { message: 'Not found' } } };
};
export const mockDel = async (url) => {
console.log(`[MOCK API] DELETE ${url}`);
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 800));
// Delete knowledge base
if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
const id = url.split('/')[2];
const index = knowledgeBases.findIndex((kb) => kb.id === id);
if (index === -1) {
throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
}
knowledgeBases.splice(index, 1);
return { success: true };
}
throw { response: { status: 404, data: { message: 'Not found' } } };
};
// Reset mock data to initial state (useful for testing)
export const resetMockData = () => {
knowledgeBases = [...mockKnowledgeBases];
};