mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 04:38:14 +08:00
[dev]knowledgebase detail page
This commit is contained in:
parent
d9e0b78bee
commit
ee5d1bcaa8
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.env*
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
@ -1846,6 +1847,11 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
|
@ -12,14 +12,15 @@ function App() {
|
|||||||
const { user } = useSelector((state) => state.auth);
|
const { user } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(login({id:111, username: 'test'}))
|
handleCheckAuth();
|
||||||
// handleCheckAuth();
|
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleCheckAuth = async () => {
|
const handleCheckAuth = async () => {
|
||||||
console.log('app handleCheckAuth');
|
console.log('app handleCheckAuth');
|
||||||
await dispatch(checkAuthThunk()).unwrap();
|
try {
|
||||||
if (user) navigate('/');
|
await dispatch(checkAuthThunk()).unwrap();
|
||||||
|
if (user) navigate('/');
|
||||||
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AppRouter></AppRouter>;
|
return <AppRouter></AppRouter>;
|
||||||
|
@ -14,6 +14,7 @@ export const icons = {
|
|||||||
p-id='2707'
|
p-id='2707'
|
||||||
width='16'
|
width='16'
|
||||||
height='16'
|
height='16'
|
||||||
|
fill='currentColor'
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d='M896 298.666667H128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h768c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667z'
|
d='M896 298.666667H128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h768c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667z'
|
||||||
@ -60,4 +61,21 @@ export const icons = {
|
|||||||
lock: `<svg xmlns='http://www.w3.org/2000/svg' height='14' width='12.25' viewBox='0 0 448 512' fill='currentColor'>
|
lock: `<svg xmlns='http://www.w3.org/2000/svg' height='14' width='12.25' viewBox='0 0 448 512' fill='currentColor'>
|
||||||
<path d='M144 144l0 48 160 0 0-48c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192l0-48C80 64.5 144.5 0 224 0s144 64.5 144 144l0 48 16 0c35.3 0 64 28.7 64 64l0 192c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 256c0-35.3 28.7-64 64-64l16 0z' />
|
<path d='M144 144l0 48 160 0 0-48c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192l0-48C80 64.5 144.5 0 224 0s144 64.5 144 144l0 48 16 0c35.3 0 64 28.7 64 64l0 192c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 256c0-35.3 28.7-64 64-64l16 0z' />
|
||||||
</svg>`,
|
</svg>`,
|
||||||
|
'stack-fill': `<svg
|
||||||
|
t='1741043402869'
|
||||||
|
class='icon'
|
||||||
|
viewBox='0 0 1024 1024'
|
||||||
|
version='1.1'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
p-id='12976'
|
||||||
|
width='20'
|
||||||
|
height='20'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d='M915.456 359.872l-403.456-276.224-403.456 276.224 403.456 276.192 403.456-276.192zM512 697.696l-356.128-234.816-47.328 49.12 403.456 276.192 403.456-276.192-47.968-50.176-355.488 235.872zM512 849.888l-356.128-234.816-47.328 49.12 403.456 276.192 403.456-276.192-47.968-50.176-355.488 235.872z'
|
||||||
|
p-id='12977'
|
||||||
|
></path>
|
||||||
|
</svg>`,
|
||||||
|
edit: `<svg t="1741043785681" class="icon" viewBox="0 0 1061 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14021" width="16" height="16" fill='currentColor'><path d="M877.714 475.429v402.286c0 40.396-32.747 73.143-73.143 73.143H146.285c-40.396 0-73.143-32.747-73.143-73.143V219.429c0-40.396 32.747-73.143 73.143-73.143h438.857V73.143H146.285C65.494 73.143-0.001 138.637-0.001 219.429v658.286c0 80.791 65.494 146.286 146.286 146.286h658.286c80.791 0 146.286-65.494 146.286-146.286V475.429h-73.143z" p-id="14022"></path><path d="M397.897 774.217c-5.145 0.812-11.079 1.275-17.121 1.275-27.052 0-51.934-9.295-71.624-24.866-24.26-24.318-23.529-59.427-22.798-117.209 2.851-45.25 21.396-85.691 50.197-116.398L830.903 22.674c40.96-40.96 100.206-20.48 138.24 16.091 10.971 10.971 40.594 40.96 51.566 51.566 36.571 36.571 58.88 96.914 17.189 138.971L543.087 724.113c-30.205 29.593-71.086 48.391-116.341 50.093l-28.848 0.01z m-36.571-75.337c13.39 1.737 28.876 2.729 44.595 2.729 6.955 0 13.864-0.194 20.723-0.577 24.676-1.644 47.559-12.193 64.931-28.534l495.854-494.76c0.004-0.236 0.007-0.514 0.007-0.793 0-14.36-6.517-27.198-16.754-35.717-11.047-10.667-41.401-41.021-52.007-51.992-8.83-10.109-21.744-16.459-36.141-16.459l-0.454 0.002-494.423 494.446a115.687 115.687 0 0 0-28.495 66.486c-0.399 6.509-0.609 13.605-0.609 20.75 0 15.659 1.007 31.082 2.961 46.209z" p-id="14023"></path></svg>`,
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { logoutThunk } from '../store/auth/auth.thunk';
|
import { logoutThunk } from '../store/auth/auth.thunk';
|
||||||
|
|
||||||
export default function HeaderWithNav() {
|
export default function HeaderWithNav() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { user } = useSelector((state) => state.auth);
|
const { user } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
dispatch(logoutThunk());
|
try {
|
||||||
|
await dispatch(logoutThunk()).unwrap();
|
||||||
|
sessionStorage.removeItem('token');
|
||||||
|
navigate('/login');
|
||||||
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className=' navbar navbar-expand-lg'>
|
<header className=' navbar navbar-expand-lg'>
|
||||||
<nav className='navbar navbar-expand-lg border-bottom p-3 mb-3 w-100'>
|
<nav className='navbar navbar-expand-lg border-bottom p-3 mb-3 w-100'>
|
||||||
<div className='container-fluid'>
|
<div className='container-fluid'>
|
||||||
<Link className='navbar-brand' to='#'>
|
<Link className='navbar-brand' to='/'>
|
||||||
OOIN 智能知识库
|
OOIN 智能知识库
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
@ -32,7 +37,7 @@ export default function HeaderWithNav() {
|
|||||||
<div className='collapse navbar-collapse' id='navbarText'>
|
<div className='collapse navbar-collapse' id='navbarText'>
|
||||||
<ul className='navbar-nav me-auto mb-lg-0'>
|
<ul className='navbar-nav me-auto mb-lg-0'>
|
||||||
<li className='nav-item'>
|
<li className='nav-item'>
|
||||||
<Link className='nav-link active' aria-current='page' to='#'>
|
<Link className='nav-link active' aria-current='page' to='/'>
|
||||||
知识库
|
知识库
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@ -50,14 +55,7 @@ export default function HeaderWithNav() {
|
|||||||
data-bs-toggle='dropdown'
|
data-bs-toggle='dropdown'
|
||||||
aria-expanded='false'
|
aria-expanded='false'
|
||||||
>
|
>
|
||||||
{/* <img
|
Hi, {user.username}
|
||||||
src='https://github.com/mdo.png'
|
|
||||||
alt='mdo'
|
|
||||||
width='32'
|
|
||||||
height='32'
|
|
||||||
className='rounded-circle'
|
|
||||||
/> */}
|
|
||||||
Hi, { user.username }
|
|
||||||
</a>
|
</a>
|
||||||
<ul
|
<ul
|
||||||
className='dropdown-menu text-small shadow'
|
className='dropdown-menu text-small shadow'
|
||||||
@ -68,16 +66,16 @@ export default function HeaderWithNav() {
|
|||||||
transform: 'translate(0px, 34px)',
|
transform: 'translate(0px, 34px)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<li>
|
{/* <li>
|
||||||
<Link className='dropdown-item' to='#'>
|
<Link className='dropdown-item' to='#'>
|
||||||
Settings
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li> */}
|
||||||
<li>
|
{/* <li>
|
||||||
<Link className='dropdown-item' to='#'>
|
<Link className='dropdown-item' to='#'>
|
||||||
Profile
|
Profile
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li> */}
|
||||||
<li>
|
<li>
|
||||||
<hr className='dropdown-divider' />
|
<hr className='dropdown-divider' />
|
||||||
</li>
|
</li>
|
||||||
|
246
src/pages/KnowledgeBase/Detail/DatasetTab.jsx
Normal file
246
src/pages/KnowledgeBase/Detail/DatasetTab.jsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import SvgIcon from '../../../components/SvgIcon';
|
||||||
|
|
||||||
|
export default function DatasetTab({ knowledgeBase }) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
const [showBatchDropdown, setShowBatchDropdown] = useState(false);
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
|
// Handle click outside dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
|
setShowBatchDropdown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
// Remove event listener on cleanup
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [dropdownRef]);
|
||||||
|
|
||||||
|
// Mock data for documents in this knowledge base
|
||||||
|
const documents = [
|
||||||
|
{
|
||||||
|
id: '1001',
|
||||||
|
name: '测试数据集 001',
|
||||||
|
description: '产品相关的所有文档和说明',
|
||||||
|
size: '124kb',
|
||||||
|
updatedAt: '2023-05-15',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1002',
|
||||||
|
name: '产品分析数据',
|
||||||
|
description: '技术架构和API文档',
|
||||||
|
size: '89kb',
|
||||||
|
updatedAt: '2023-05-10',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle select all checkbox
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (selectAll) {
|
||||||
|
setSelectedDocuments([]);
|
||||||
|
} else {
|
||||||
|
setSelectedDocuments(documents.map((doc) => doc.id));
|
||||||
|
}
|
||||||
|
setSelectAll(!selectAll);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle individual document selection
|
||||||
|
const handleSelectDocument = (docId) => {
|
||||||
|
if (selectedDocuments.includes(docId)) {
|
||||||
|
setSelectedDocuments(selectedDocuments.filter((id) => id !== docId));
|
||||||
|
setSelectAll(false);
|
||||||
|
} else {
|
||||||
|
setSelectedDocuments([...selectedDocuments, docId]);
|
||||||
|
if (selectedDocuments.length + 1 === documents.length) {
|
||||||
|
setSelectAll(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle batch delete
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
if (selectedDocuments.length === 0) return;
|
||||||
|
|
||||||
|
// Here you would typically call an API to delete the selected documents
|
||||||
|
console.log('Deleting documents:', selectedDocuments);
|
||||||
|
|
||||||
|
// Reset selection
|
||||||
|
setSelectedDocuments([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
setShowBatchDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Breadcrumb navigation */}
|
||||||
|
<div className='d-flex align-items-center mb-4 mt-3'>
|
||||||
|
<nav aria-label='breadcrumb'>
|
||||||
|
<ol className='breadcrumb mb-0'>
|
||||||
|
<li className='breadcrumb-item'>
|
||||||
|
<Link className='text-secondary text-decoration-none' to='/'>
|
||||||
|
知识库
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className='breadcrumb-item'>
|
||||||
|
<Link className='text-secondary text-decoration-none' to={`/knowledge-base/${knowledgeBase.id}`}>{knowledgeBase.title}</Link>
|
||||||
|
</li>
|
||||||
|
<li className='breadcrumb-item active text-dark' aria-current='page'>
|
||||||
|
数据集
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action bar */}
|
||||||
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||||
|
<div className='d-flex align-items-center'>
|
||||||
|
<div className='dropdown me-2 position-relative' ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
className={`btn ${
|
||||||
|
selectedDocuments.length > 0 ? 'btn-outline-dark' : 'btn-outline-secondary'
|
||||||
|
} dropdown-toggle d-flex align-items-center gap-1`}
|
||||||
|
type='button'
|
||||||
|
onClick={() => setShowBatchDropdown(!showBatchDropdown)}
|
||||||
|
aria-expanded={showBatchDropdown}
|
||||||
|
disabled={selectedDocuments.length === 0}
|
||||||
|
>
|
||||||
|
<SvgIcon className='stack-fill' />
|
||||||
|
批量操作 {selectedDocuments.length > 0 ? `(${selectedDocuments.length})` : ''}
|
||||||
|
</button>
|
||||||
|
{showBatchDropdown && (
|
||||||
|
<ul className='dropdown-menu shadow show' style={{ position: 'absolute', zIndex: 1000 }}>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className='dropdown-item d-flex align-items-center text-danger'
|
||||||
|
onClick={handleBatchDelete}
|
||||||
|
>
|
||||||
|
<SvgIcon className='trash me-2' />
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='position-relative'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='form-control'
|
||||||
|
placeholder='搜索文件...'
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className='position-absolute top-50 end-0 translate-middle-y pe-3'>
|
||||||
|
<SvgIcon className='search' />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className='btn btn-dark d-flex align-items-center gap-1'>
|
||||||
|
<SvgIcon className='plus' />
|
||||||
|
新增文件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Documents table */}
|
||||||
|
<div className='card border-0 shadow-sm'>
|
||||||
|
<div className='card-body p-0'>
|
||||||
|
<table className='table table-hover mb-0'>
|
||||||
|
<thead className='table-light'>
|
||||||
|
<tr>
|
||||||
|
<th scope='col' width='40'>
|
||||||
|
<div className='form-check'>
|
||||||
|
<input
|
||||||
|
className='form-check-input'
|
||||||
|
type='checkbox'
|
||||||
|
id='selectAllCheckbox'
|
||||||
|
checked={selectAll}
|
||||||
|
onChange={handleSelectAll}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th scope='col'>ID</th>
|
||||||
|
<th scope='col'>名称</th>
|
||||||
|
<th scope='col'>描述</th>
|
||||||
|
<th scope='col'>文档大小</th>
|
||||||
|
<th scope='col'>更新日期</th>
|
||||||
|
<th scope='col'>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{documents.map((doc) => (
|
||||||
|
<tr key={doc.id}>
|
||||||
|
<td>
|
||||||
|
<div className='form-check'>
|
||||||
|
<input
|
||||||
|
className='form-check-input'
|
||||||
|
type='checkbox'
|
||||||
|
id={`checkbox-${doc.id}`}
|
||||||
|
checked={selectedDocuments.includes(doc.id)}
|
||||||
|
onChange={() => handleSelectDocument(doc.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>#{doc.id}</td>
|
||||||
|
<td>{doc.name}</td>
|
||||||
|
<td>{doc.description}</td>
|
||||||
|
<td>{doc.size}</td>
|
||||||
|
<td>{doc.updatedAt}</td>
|
||||||
|
<td>
|
||||||
|
<div className='d-flex gap-2'>
|
||||||
|
<button className='btn btn-sm btn-outline-primary' title='查看'>
|
||||||
|
<SvgIcon className='eye' />
|
||||||
|
</button>
|
||||||
|
<button className='btn btn-sm btn-outline-success' title='编辑'>
|
||||||
|
<SvgIcon className='edit' />
|
||||||
|
</button>
|
||||||
|
<button className='btn btn-sm btn-outline-danger' title='删除'>
|
||||||
|
<SvgIcon className='trash' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className='d-flex justify-content-between align-items-center mt-3'>
|
||||||
|
<div>
|
||||||
|
每页行数:
|
||||||
|
<select className='form-select form-select-sm 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-sm mb-0'>
|
||||||
|
<li className='page-item'>
|
||||||
|
<button className='page-link' aria-label='Previous'>
|
||||||
|
<span aria-hidden='true'>«</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className='page-item'>
|
||||||
|
<button className='page-link' aria-label='Next'>
|
||||||
|
<span aria-hidden='true'>»</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
189
src/pages/KnowledgeBase/Detail/SettingsTab.jsx
Normal file
189
src/pages/KnowledgeBase/Detail/SettingsTab.jsx
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import SvgIcon from '../../../components/SvgIcon';
|
||||||
|
|
||||||
|
// Custom styled Link component with hover effect
|
||||||
|
const StyledLink = ({ to, children }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
className='text-secondary text-decoration-none'
|
||||||
|
style={{ transition: 'all 0.2s ease' }}
|
||||||
|
onMouseOver={(e) => (e.currentTarget.style.color = '#212529')}
|
||||||
|
onMouseOut={(e) => (e.currentTarget.style.color = '')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsTab({ knowledgeBase }) {
|
||||||
|
// Handle form submission
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Here you would typically call an API to update the knowledge base settings
|
||||||
|
console.log('Updating knowledge base settings');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle knowledge base deletion
|
||||||
|
const handleDelete = () => {
|
||||||
|
// Here you would typically call an API to delete the knowledge base
|
||||||
|
console.log('Deleting knowledge base:', knowledgeBase.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Breadcrumb navigation */}
|
||||||
|
<div className='d-flex align-items-center mb-4 mt-3'>
|
||||||
|
<nav aria-label='breadcrumb'>
|
||||||
|
<ol className='breadcrumb mb-0'>
|
||||||
|
<li className='breadcrumb-item'>
|
||||||
|
<StyledLink to='/'>知识库</StyledLink>
|
||||||
|
</li>
|
||||||
|
<li className='breadcrumb-item'>
|
||||||
|
<StyledLink to={`/knowledge-base/${knowledgeBase.id}`}>{knowledgeBase.title}</StyledLink>
|
||||||
|
</li>
|
||||||
|
<li className='breadcrumb-item active text-secondary' aria-current='page'>
|
||||||
|
设置
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Settings form */}
|
||||||
|
<div className='card border-0 shadow-sm'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<h5 className='card-title mb-4'>知识库设置</h5>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor='knowledgeTitle' className='form-label'>
|
||||||
|
知识库名称
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='form-control'
|
||||||
|
id='knowledgeTitle'
|
||||||
|
defaultValue={knowledgeBase.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor='knowledgeDescription' className='form-label'>
|
||||||
|
知识库描述
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className='form-control'
|
||||||
|
id='knowledgeDescription'
|
||||||
|
rows='3'
|
||||||
|
defaultValue={knowledgeBase.description}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label className='form-label'>访问权限</label>
|
||||||
|
<div className='form-check'>
|
||||||
|
<input
|
||||||
|
className='form-check-input'
|
||||||
|
type='radio'
|
||||||
|
name='accessPermission'
|
||||||
|
id='accessPublic'
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<label className='form-check-label' htmlFor='accessPublic'>
|
||||||
|
公开 - 所有人可访问
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='form-check'>
|
||||||
|
<input
|
||||||
|
className='form-check-input'
|
||||||
|
type='radio'
|
||||||
|
name='accessPermission'
|
||||||
|
id='accessPrivate'
|
||||||
|
/>
|
||||||
|
<label className='form-check-label' htmlFor='accessPrivate'>
|
||||||
|
私有 - 仅创建者可访问
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label className='form-label'>权限管理</label>
|
||||||
|
<div className='card'>
|
||||||
|
<div className='card-body p-0'>
|
||||||
|
<table className='table mb-0'>
|
||||||
|
<thead className='table-light'>
|
||||||
|
<tr>
|
||||||
|
<th scope='col'>ID</th>
|
||||||
|
<th scope='col'>用户名</th>
|
||||||
|
<th scope='col'>邮箱</th>
|
||||||
|
<th scope='col'>权限类型</th>
|
||||||
|
<th scope='col'>访问时长</th>
|
||||||
|
<th scope='col'>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>#1001</td>
|
||||||
|
<td>测试数据集 001</td>
|
||||||
|
<td>zhang@abc.com</td>
|
||||||
|
<td>只读</td>
|
||||||
|
<td>一个月</td>
|
||||||
|
<td>
|
||||||
|
<div className='d-flex gap-2'>
|
||||||
|
<button
|
||||||
|
className='btn btn-sm btn-outline-secondary'
|
||||||
|
title='编辑'
|
||||||
|
>
|
||||||
|
<SvgIcon className='edit' />
|
||||||
|
</button>
|
||||||
|
<button className='btn btn-sm btn-outline-danger' title='删除'>
|
||||||
|
<SvgIcon className='trash' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>#1002</td>
|
||||||
|
<td>产品分析数据</td>
|
||||||
|
<td>li@abc.com</td>
|
||||||
|
<td>完全访问</td>
|
||||||
|
<td>永久</td>
|
||||||
|
<td>
|
||||||
|
<div className='d-flex gap-2'>
|
||||||
|
<button
|
||||||
|
className='btn btn-sm btn-outline-secondary'
|
||||||
|
title='编辑'
|
||||||
|
>
|
||||||
|
<SvgIcon className='edit' />
|
||||||
|
</button>
|
||||||
|
<button className='btn btn-sm btn-outline-danger' title='删除'>
|
||||||
|
<SvgIcon className='trash' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type='button' className='btn btn-outline-primary mt-2'>
|
||||||
|
<SvgIcon className='plus me-1' />
|
||||||
|
添加用户
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='d-flex justify-content-end gap-2 mt-4'>
|
||||||
|
<button type='button' className='btn btn-outline-danger' onClick={handleDelete}>
|
||||||
|
删除知识库
|
||||||
|
</button>
|
||||||
|
<button type='submit' className='btn btn-primary'>
|
||||||
|
保存设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import KnowledgeCard from './KnowledgeCard';
|
import KnowledgeCard from './KnowledgeCard';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { showNotification } from '../../store/notification.slice';
|
import { showNotification } from '../../store/notification.slice';
|
||||||
@ -6,9 +7,16 @@ import SvgIcon from '../../components/SvgIcon';
|
|||||||
|
|
||||||
export default function KnowledgeBase() {
|
export default function KnowledgeBase() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
const knowledgeList = [
|
const knowledgeList = [
|
||||||
{
|
{
|
||||||
|
id: '1',
|
||||||
title: '产品开发知识库',
|
title: '产品开发知识库',
|
||||||
description: '产品开发流程及规范说明文档',
|
description: '产品开发流程及规范说明文档',
|
||||||
documents: 24,
|
documents: 24,
|
||||||
@ -16,30 +24,168 @@ export default function KnowledgeBase() {
|
|||||||
access: 'full',
|
access: 'full',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: '2',
|
||||||
title: '市场分析知识库',
|
title: '市场分析知识库',
|
||||||
description: '2025年Q1市场分析总结',
|
description: '2025年Q1市场分析总结',
|
||||||
documents: 12,
|
documents: 12,
|
||||||
date: '2025-02-10',
|
date: '2025-02-10',
|
||||||
access: 'read',
|
access: 'read',
|
||||||
},
|
},
|
||||||
{ title: '财务知识库', description: '月度财务分析报告', documents: 8, date: '2025-02-01', access: 'none' },
|
{
|
||||||
|
id: '3',
|
||||||
|
title: '财务知识库',
|
||||||
|
description: '月度财务分析报告',
|
||||||
|
documents: 8,
|
||||||
|
date: '2025-02-01',
|
||||||
|
access: 'none',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setNewKnowledgeBase((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateKnowledgeBase = () => {
|
||||||
|
// Here you would typically call an API to create the knowledge base
|
||||||
|
if (!newKnowledgeBase.title.trim()) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '请输入知识库名称',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, just show a success notification
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '知识库创建成功',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// In a real application, you would get the ID from the API response
|
||||||
|
// For now, we'll generate a mock ID
|
||||||
|
const newId = Date.now().toString();
|
||||||
|
|
||||||
|
// Reset form and close modal
|
||||||
|
setNewKnowledgeBase({ title: '', description: '' });
|
||||||
|
setShowCreateModal(false);
|
||||||
|
|
||||||
|
// Navigate to the newly created knowledge base with datasets tab
|
||||||
|
navigate(`/knowledge-base/${newId}/datasets`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = (id) => {
|
||||||
|
navigate(`/knowledge-base/${id}/datasets`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||||
<input type='text' className='form-control w-50' placeholder='搜索知识库...' />
|
<input type='text' className='form-control w-50' placeholder='搜索知识库...' />
|
||||||
<button className='btn btn-dark d-flex align-items-center gap-1'>
|
<button
|
||||||
|
className='btn btn-dark d-flex align-items-center gap-1'
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
>
|
||||||
<SvgIcon className={'plus'} />
|
<SvgIcon className={'plus'} />
|
||||||
新建知识库
|
新建知识库
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='row g-3'>
|
<div className='row gap-3 m-0'>
|
||||||
{knowledgeList.map((item, index) => (
|
{knowledgeList.map((item) => (
|
||||||
<KnowledgeCard key={index} {...item} />
|
<React.Fragment key={item.id}>
|
||||||
|
<KnowledgeCard {...item} onClick={() => handleCardClick(item.id)} />
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 新建知识库弹窗 */}
|
||||||
|
{showCreateModal && (
|
||||||
|
<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={() => setShowCreateModal(false)}
|
||||||
|
aria-label='Close'
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div className='modal-body'>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor='knowledgeTitle' className='form-label'>
|
||||||
|
知识库名称
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='form-control'
|
||||||
|
id='knowledgeTitle'
|
||||||
|
name='title'
|
||||||
|
value={newKnowledgeBase.title}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder='请输入知识库名称'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor='knowledgeDescription' className='form-label'>
|
||||||
|
知识库描述
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className='form-control'
|
||||||
|
id='knowledgeDescription'
|
||||||
|
name='description'
|
||||||
|
value={newKnowledgeBase.description}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder='请输入知识库描述'
|
||||||
|
rows='3'
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='modal-footer d-flex justify-content-end gap-2'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-outline-secondary'
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button type='button' className='btn btn-dark' onClick={handleCreateKnowledgeBase}>
|
||||||
|
创建
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
89
src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx
Normal file
89
src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
|
import DatasetTab from './Detail/DatasetTab';
|
||||||
|
import SettingsTab from './Detail/SettingsTab';
|
||||||
|
|
||||||
|
export default function KnowledgeBaseDetail() {
|
||||||
|
const { id, tab } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
|
||||||
|
|
||||||
|
// Update active tab when URL changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (tab) {
|
||||||
|
setActiveTab(tab === 'settings' ? 'settings' : 'datasets');
|
||||||
|
}
|
||||||
|
}, [tab]);
|
||||||
|
|
||||||
|
// Mock data for the knowledge base details
|
||||||
|
const knowledgeBase = {
|
||||||
|
id: id,
|
||||||
|
title: '知识库 1',
|
||||||
|
description: '知识库详细信息',
|
||||||
|
createdAt: '2023-05-01',
|
||||||
|
updatedAt: '2023-05-15',
|
||||||
|
documentsCount: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle tab change
|
||||||
|
const handleTabChange = (tab) => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
navigate(`/knowledge-base/${id}/${tab}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='container-fluid px-4'>
|
||||||
|
<div className='row'>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className='col-md-3 col-lg-2 border-end'>
|
||||||
|
<div className='py-4'>
|
||||||
|
<div className='h4 mb-3 text-center'>{knowledgeBase.title}</div>
|
||||||
|
<p className='text-center text-muted small mb-4'>{knowledgeBase.description}</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<nav className='nav flex-column'>
|
||||||
|
<a
|
||||||
|
className={`nav-link link-dark link-underline-light d-flex align-items-center ${
|
||||||
|
activeTab === 'datasets' ? 'active bg-light rounded fw-bold' : ''
|
||||||
|
}`}
|
||||||
|
href='#'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleTabChange('datasets');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SvgIcon className='database me-2' />
|
||||||
|
数据集
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className={`nav-link link-dark link-underline-light d-flex align-items-center ${
|
||||||
|
activeTab === 'settings' ? 'active bg-light rounded fw-bold' : ''
|
||||||
|
}`}
|
||||||
|
href='#'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleTabChange('settings');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SvgIcon className='gear me-2' />
|
||||||
|
设置
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className='col-md-9 col-lg-10'>
|
||||||
|
{/* Render the appropriate tab component */}
|
||||||
|
{activeTab === 'datasets' ? (
|
||||||
|
<DatasetTab knowledgeBase={knowledgeBase} />
|
||||||
|
) : (
|
||||||
|
<SettingsTab knowledgeBase={knowledgeBase} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,61 +1,59 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SvgIcon from '../../components/SvgIcon';
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
|
|
||||||
export default function KnowledgeCard({ title, description, documents, date, access }) {
|
export default function KnowledgeCard({ id, title, description, documents, date, access, onClick }) {
|
||||||
return (
|
return (
|
||||||
<div className='col-sm-6 col-md-6 col-lg-4 col-xl-3'>
|
<div className='knowledge-card card shadow border-0 p-0 col' onClick={onClick}>
|
||||||
<div className='knowledge-card card shadow border-0'>
|
<div className='card-body'>
|
||||||
<div className='card-body'>
|
<h5 className='card-title'>{title}</h5>
|
||||||
<h5 className='card-title'>{title}</h5>
|
<div className='hoverdown position-absolute end-0 top-0'>
|
||||||
<div className='hoverdown position-absolute end-0 top-0'>
|
<button type='button' className='detail-btn btn'>
|
||||||
<button type='button' className='detail-btn btn'>
|
<SvgIcon className={'more-dot'} />
|
||||||
<SvgIcon className={'more-dot'} />
|
</button>
|
||||||
</button>
|
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
|
||||||
<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'>
|
删除
|
||||||
删除
|
<SvgIcon className={'trash'} />
|
||||||
<SvgIcon className={'trash'} />
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
<p className='card-text text-muted'>{description}</p>
|
||||||
<p className='card-text text-muted'>{description}</p>
|
<div className='text-muted d-flex align-items-center gap-1'>
|
||||||
<div className='text-muted d-flex align-items-center gap-1'>
|
<SvgIcon className={'file'} />
|
||||||
<SvgIcon className={'file'} />
|
{documents} 文档
|
||||||
{documents} 文档
|
<span className='ms-3 d-flex align-items-center gap-1'>
|
||||||
<span className='ms-3 d-flex align-items-center gap-1'>
|
<SvgIcon className={'clock'} />
|
||||||
<SvgIcon className={'clock'} />
|
{date}
|
||||||
{date}
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='mt-3 d-flex justify-content-between align-items-end'>
|
||||||
|
{access === 'full' ? (
|
||||||
|
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
|
||||||
|
<SvgIcon className={'circle-yes'} />
|
||||||
|
完全访问
|
||||||
</span>
|
</span>
|
||||||
</div>
|
) : access === 'read' ? (
|
||||||
<div className='mt-3 d-flex justify-content-between align-items-end'>
|
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
|
||||||
{access === 'full' ? (
|
<SvgIcon className={'eye'} />
|
||||||
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
|
只读访问
|
||||||
<SvgIcon className={'circle-yes'} />
|
</span>
|
||||||
完全访问
|
) : (
|
||||||
</span>
|
<span className='badge bg-dark-subtle d-flex align-items-center gap-1'>
|
||||||
) : access === 'read' ? (
|
<SvgIcon className={'lock'} />
|
||||||
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
|
无访问权限
|
||||||
<SvgIcon className={'eye'} />
|
</span>
|
||||||
只读访问
|
)}
|
||||||
</span>
|
{access === 'full' || access === 'read' ? (
|
||||||
) : (
|
<button className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'>
|
||||||
<span className='badge bg-dark-subtle d-flex align-items-center gap-1'>
|
<SvgIcon className={'chat-dot'} />
|
||||||
<SvgIcon className={'lock'} />
|
新聊天
|
||||||
无访问权限
|
</button>
|
||||||
</span>
|
) : (
|
||||||
)}
|
<button className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'>
|
||||||
{access === 'full' || access === 'read' ? (
|
<SvgIcon className={'key'} />
|
||||||
<button className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'>
|
申请权限
|
||||||
<SvgIcon className={'chat-dot'} />
|
</button>
|
||||||
新聊天
|
)}
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button className='btn btn-outline-dark btn-sm d-flex align-items-center gap-1'>
|
|
||||||
<SvgIcon className={'key'} />
|
|
||||||
申请权限
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,8 +6,8 @@ import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
|
|||||||
export default function Login() {
|
export default function Login() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('member2');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('member123');
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
@ -18,9 +18,11 @@ export default function Login() {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleCheckAuth = async () => {
|
const handleCheckAuth = async () => {
|
||||||
console.log('login page handleCheckAuth');
|
console.log('login page handleCheckAuth');
|
||||||
await dispatch(checkAuthThunk()).unwrap();
|
try {
|
||||||
if (user) navigate('/');
|
await dispatch(checkAuthThunk()).unwrap();
|
||||||
|
if (user) navigate('/');
|
||||||
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
@ -37,17 +39,20 @@ export default function Login() {
|
|||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
console.log(validateForm());
|
|
||||||
|
|
||||||
if (validateForm()) {
|
if (validateForm()) {
|
||||||
console.log('Form submitted successfully!');
|
console.log('Form submitted successfully!');
|
||||||
console.log('Username:', username);
|
console.log('Username:', username);
|
||||||
console.log('Password:', password);
|
console.log('Password:', password);
|
||||||
|
try {
|
||||||
dispatch(loginThunk({ username, password }));
|
await dispatch(loginThunk({ username, password })).unwrap();
|
||||||
|
navigate('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@ -60,6 +65,7 @@ export default function Login() {
|
|||||||
>
|
>
|
||||||
<div className='input-group has-validation'>
|
<div className='input-group has-validation'>
|
||||||
<input
|
<input
|
||||||
|
value={username}
|
||||||
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'
|
||||||
@ -71,6 +77,7 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
<div className='input-group has-validation'>
|
<div className='input-group has-validation'>
|
||||||
<input
|
<input
|
||||||
|
value={password}
|
||||||
type='password'
|
type='password'
|
||||||
id='password'
|
id='password'
|
||||||
placeholder='Password'
|
placeholder='Password'
|
||||||
|
@ -20,8 +20,10 @@ export default function Signup() {
|
|||||||
|
|
||||||
const handleCheckAuth = async () => {
|
const handleCheckAuth = async () => {
|
||||||
console.log('signup page handleCheckAuth');
|
console.log('signup page handleCheckAuth');
|
||||||
await dispatch(checkAuthThunk()).unwrap();
|
try {
|
||||||
if (user) navigate('/');
|
await dispatch(checkAuthThunk()).unwrap();
|
||||||
|
if (user) navigate('/');
|
||||||
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
@ -35,7 +37,7 @@ export default function Signup() {
|
|||||||
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email)) {
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email)) {
|
||||||
newErrors.email = 'Invalid email address';
|
newErrors.email = 'Invalid email address';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password) {
|
if (!password) {
|
||||||
newErrors.password = 'Password is required';
|
newErrors.password = 'Password is required';
|
||||||
} else if (password.length < 6) {
|
} else if (password.length < 6) {
|
||||||
@ -45,7 +47,7 @@ export default function Signup() {
|
|||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
console.log(validateForm());
|
console.log(validateForm());
|
||||||
@ -55,8 +57,12 @@ export default function Signup() {
|
|||||||
console.log('Username:', username);
|
console.log('Username:', username);
|
||||||
console.log('Email:', email);
|
console.log('Email:', email);
|
||||||
console.log('Password:', password);
|
console.log('Password:', password);
|
||||||
|
try {
|
||||||
dispatch(signupThunk({ username, password, email }));
|
await dispatch(signupThunk({ username, password, email })).unwrap();
|
||||||
|
navigate('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Signup failed:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,14 +2,15 @@ import React, { Suspense } from 'react';
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import Mainlayout from '../layouts/Mainlayout';
|
import Mainlayout from '../layouts/Mainlayout';
|
||||||
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
|
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
|
||||||
|
import KnowledgeBaseDetail from '../pages/KnowledgeBase/KnowledgeBaseDetail';
|
||||||
import Loading from '../components/Loading';
|
import Loading from '../components/Loading';
|
||||||
import Login from '../pages/auth/Login';
|
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';
|
||||||
|
|
||||||
function AppRouter() {
|
function AppRouter() {
|
||||||
const { id } = useSelector((state) => state.auth);
|
const { user } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
@ -23,10 +24,26 @@ function AppRouter() {
|
|||||||
</Mainlayout>
|
</Mainlayout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/knowledge-base/:id'
|
||||||
|
element={
|
||||||
|
<Mainlayout>
|
||||||
|
<KnowledgeBaseDetail />
|
||||||
|
</Mainlayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='/knowledge-base/:id/:tab'
|
||||||
|
element={
|
||||||
|
<Mainlayout>
|
||||||
|
<KnowledgeBaseDetail />
|
||||||
|
</Mainlayout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path='/login' element={<Login />} />
|
<Route path='/login' element={<Login />} />
|
||||||
<Route path='/signup' element={<Signup />} />
|
<Route path='/signup' element={<Signup />} />
|
||||||
<Route path='*' element={<Navigate to={!!id ? '/' : '/login'} replace />} />
|
<Route path='*' element={<Navigate to={!!user ? '/' : '/login'} replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
|
const secretKey = import.meta.env.VITE_SECRETKEY;
|
||||||
|
|
||||||
// Create Axios instance with base URL
|
// Create Axios instance with base URL
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'api',
|
baseURL: '/api',
|
||||||
withCredentials: true, // Include cookies if needed
|
withCredentials: true, // Include cookies if needed
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request Interceptor
|
// Request Interceptor
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
const encryptedToken = sessionStorage.getItem('token') || '';
|
||||||
|
if (encryptedToken) {
|
||||||
|
const decryptedToken = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8);
|
||||||
|
config.headers.Authorization = `Token ${decryptedToken}`;
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@ -26,8 +34,8 @@ api.interceptors.response.use(
|
|||||||
// Handle errors in the response
|
// Handle errors in the response
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// monitor /verify
|
// monitor /verify
|
||||||
if (error.response.status === 401 && error.config.url === '/check-token') {
|
if (error.response.status === 401 && error.config.url === '/check-token/') {
|
||||||
if (window.location.pathname !== '/login' && window.location.pathname !== '/register') {
|
if (window.location.pathname !== '/login' && window.location.pathname !== '/signup') {
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,7 +66,7 @@ const get = async (url, params = {}) => {
|
|||||||
const post = async (url, data, isMultipart = false) => {
|
const post = async (url, data, isMultipart = false) => {
|
||||||
const headers = isMultipart
|
const headers = isMultipart
|
||||||
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
||||||
: { 'Content-Type': 'application/json' }; // For JSON data
|
: { 'Content-Type': 'application/json' }; // For JSON data
|
||||||
|
|
||||||
const res = await api.post(url, data, { headers });
|
const res = await api.post(url, data, { headers });
|
||||||
return res.data;
|
return res.data;
|
||||||
|
@ -20,7 +20,7 @@ const setRejected = (state, action) => {
|
|||||||
|
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
initialState: { loading: false, error: null, user: {id: 123, username: 'test'} },
|
initialState: { loading: false, error: null, user: null },
|
||||||
reducers: {
|
reducers: {
|
||||||
login: (state, action) => {
|
login: (state, action) => {
|
||||||
state.user = action.payload;
|
state.user = action.payload;
|
||||||
|
@ -2,15 +2,22 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
|||||||
import { get, post } from '../../services/api';
|
import { get, post } from '../../services/api';
|
||||||
import { showNotification } from '../notification.slice';
|
import { showNotification } from '../notification.slice';
|
||||||
import { logout } from './auth.slice';
|
import { logout } from './auth.slice';
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
|
const secretKey = import.meta.env.VITE_SECRETKEY;
|
||||||
|
|
||||||
export const loginThunk = createAsyncThunk(
|
export const loginThunk = createAsyncThunk(
|
||||||
'auth/login',
|
'auth/login',
|
||||||
async ({ username, password }, { rejectWithValue, dispatch }) => {
|
async ({ username, password }, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
const { message, user } = await post('/login', { username, password });
|
const { message, user, token } = await post('/login/', { username, password });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(message || 'Something went wrong');
|
throw new Error(message || 'Something went wrong');
|
||||||
}
|
}
|
||||||
|
// encrypt token
|
||||||
|
const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
|
||||||
|
sessionStorage.setItem('token', encryptedToken);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.message || 'Something went wrong';
|
const errorMessage = error.response?.data?.message || 'Something went wrong';
|
||||||
@ -46,7 +53,7 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje
|
|||||||
|
|
||||||
export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectWithValue, dispatch }) => {
|
export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
const { user, message } = await get('/check-token');
|
const { user, message } = await get('/check-token/');
|
||||||
if (!user) {
|
if (!user) {
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
throw new Error(message || 'No token found');
|
throw new Error(message || 'No token found');
|
||||||
@ -62,7 +69,7 @@ export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectW
|
|||||||
export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => {
|
export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
// Send the logout request to the server (this assumes your server clears any session-related info)
|
// Send the logout request to the server (this assumes your server clears any session-related info)
|
||||||
await post('/logout');
|
await post('/logout/');
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.message || 'Log out failed';
|
const errorMessage = error.response?.data?.message || 'Log out failed';
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
@import 'bootstrap/scss/bootstrap';
|
@import 'bootstrap/scss/bootstrap';
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-width: 24rem;
|
||||||
|
}
|
||||||
.dropdown-toggle {
|
.dropdown-toggle {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
@ -9,6 +12,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-card {
|
.knowledge-card {
|
||||||
|
min-width: 20rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.hoverdown:hover .hoverdown-menu{
|
.hoverdown:hover .hoverdown-menu{
|
||||||
display: block;
|
display: block;
|
||||||
color: red;
|
color: red;
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [react()],
|
const env = loadEnv(mode, process.cwd());
|
||||||
build: {
|
|
||||||
outDir: '../dist'
|
return {
|
||||||
},
|
plugins: [react()],
|
||||||
server: {
|
build: {
|
||||||
port: 8080
|
outDir: '../dist',
|
||||||
}
|
},
|
||||||
})
|
server: {
|
||||||
|
port: env.VITE_PORT,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: env.VITE_API_URL || 'http://124.222.236.141:58000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user