From 445c30ba0b0ed826be1dcbc4b06da9ee2e04c967 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Fri, 28 Feb 2025 17:05:36 -0500 Subject: [PATCH 01/21] [dev]change responsive header --- src/layouts/HeaderWithNav.jsx | 109 ++++++++++++---------------------- 1 file changed, 39 insertions(+), 70 deletions(-) diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx index 835935f..083f734 100644 --- a/src/layouts/HeaderWithNav.jsx +++ b/src/layouts/HeaderWithNav.jsx @@ -2,83 +2,52 @@ import React from 'react'; export default function HeaderWithNav() { return ( -
-
-
- - {/* - - */} - OOIN 智能知识库 +
+
+
); } From f88a14ee31467daa2d4c5c015f5e502a93c2eab9 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Fri, 28 Feb 2025 19:34:56 -0500 Subject: [PATCH 02/21] [dev]set svg icons lib --- src/components/Snackbar.jsx | 24 +----- src/components/SvgIcon.jsx | 12 +++ src/icons/icons.js | 63 ++++++++++++++++ src/layouts/HeaderWithNav.jsx | 34 ++++----- src/pages/KnowledgeBase/KnowledgeBase.jsx | 9 +-- src/pages/KnowledgeBase/KnowledgeCard.jsx | 91 +++-------------------- src/styles/style.scss | 1 + 7 files changed, 109 insertions(+), 125 deletions(-) create mode 100644 src/components/SvgIcon.jsx create mode 100644 src/icons/icons.js diff --git a/src/components/Snackbar.jsx b/src/components/Snackbar.jsx index 1b84c48..c007395 100644 --- a/src/components/Snackbar.jsx +++ b/src/components/Snackbar.jsx @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; +import SvgIcon from './SvgIcon'; const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => { if (!message) return null; @@ -21,32 +22,11 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => { return ( <> - - - - - - - - - - - -
- - - +
{message}
- + + {/* Documents table */} +
+
+ + + + + + + + + + + + + + {documents.map((doc) => ( + + + + + + + + + + ))} + +
+
+ +
+
ID名称描述文档大小更新日期操作
+
+ handleSelectDocument(doc.id)} + /> +
+
#{doc.id}{doc.name}{doc.description}{doc.size}{doc.updatedAt} +
+ + + +
+
+
+
+ + {/* Pagination */} +
+
+ 每页行数: + +
+
+ 1-5 of 10 + +
+
+ + ); +} diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx new file mode 100644 index 0000000..114c98e --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -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 ( + (e.currentTarget.style.color = '#212529')} + onMouseOut={(e) => (e.currentTarget.style.color = '')} + > + {children} + + ); +}; + +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 */} +
+ +
+ + {/* Settings form */} +
+
+
知识库设置
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID用户名邮箱权限类型访问时长操作
#1001测试数据集 001zhang@abc.com只读一个月 +
+ + +
+
#1002产品分析数据li@abc.com完全访问永久 +
+ + +
+
+
+
+ +
+ +
+ + +
+
+
+
+ + ); +} diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index 8c46cb2..d789dec 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import KnowledgeCard from './KnowledgeCard'; import { useDispatch } from 'react-redux'; import { showNotification } from '../../store/notification.slice'; @@ -6,9 +7,16 @@ import SvgIcon from '../../components/SvgIcon'; export default function KnowledgeBase() { const dispatch = useDispatch(); + const navigate = useNavigate(); + const [showCreateModal, setShowCreateModal] = useState(false); + const [newKnowledgeBase, setNewKnowledgeBase] = useState({ + title: '', + description: '', + }); const knowledgeList = [ { + id: '1', title: '产品开发知识库', description: '产品开发流程及规范说明文档', documents: 24, @@ -16,30 +24,168 @@ export default function KnowledgeBase() { access: 'full', }, { + id: '2', title: '市场分析知识库', description: '2025年Q1市场分析总结', documents: 12, date: '2025-02-10', 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 (
-
-
- {knowledgeList.map((item, index) => ( - +
+ {knowledgeList.map((item) => ( + + handleCardClick(item.id)} /> + ))}
+ + {/* 新建知识库弹窗 */} + {showCreateModal && ( +
+
+
+
新建知识库
+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ )}
); } diff --git a/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx new file mode 100644 index 0000000..5c0b033 --- /dev/null +++ b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx @@ -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 ( +
+
+ {/* Sidebar */} + + + {/* Main content */} +
+ {/* Render the appropriate tab component */} + {activeTab === 'datasets' ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/src/pages/KnowledgeBase/KnowledgeCard.jsx b/src/pages/KnowledgeBase/KnowledgeCard.jsx index 928e20e..bf0f0d0 100644 --- a/src/pages/KnowledgeBase/KnowledgeCard.jsx +++ b/src/pages/KnowledgeBase/KnowledgeCard.jsx @@ -1,61 +1,59 @@ import React from 'react'; 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 ( -
-
-
-
{title}
-
- -
    -
  • - 删除 - -
  • -
-
-

{description}

-
- - {documents} 文档 - - - {date} +
+
+
{title}
+
+ +
    +
  • + 删除 + +
  • +
+
+

{description}

+
+ + {documents} 文档 + + + {date} + +
+
+ {access === 'full' ? ( + + + 完全访问 -
-
- {access === 'full' ? ( - - - 完全访问 - - ) : access === 'read' ? ( - - - 只读访问 - - ) : ( - - - 无访问权限 - - )} - {access === 'full' || access === 'read' ? ( - - ) : ( - - )} -
+ ) : access === 'read' ? ( + + + 只读访问 + + ) : ( + + + 无访问权限 + + )} + {access === 'full' || access === 'read' ? ( + + ) : ( + + )}
diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.jsx index 334dcb3..0183a9d 100644 --- a/src/pages/auth/Login.jsx +++ b/src/pages/auth/Login.jsx @@ -6,8 +6,8 @@ import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk'; export default function Login() { const dispatch = useDispatch(); const navigate = useNavigate(); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); + const [username, setUsername] = useState('member2'); + const [password, setPassword] = useState('member123'); const [errors, setErrors] = useState({}); const [submitted, setSubmitted] = useState(false); @@ -18,9 +18,11 @@ export default function Login() { }, [dispatch]); const handleCheckAuth = async () => { - console.log('login page handleCheckAuth'); - await dispatch(checkAuthThunk()).unwrap(); - if (user) navigate('/'); + console.log('login page handleCheckAuth'); + try { + await dispatch(checkAuthThunk()).unwrap(); + if (user) navigate('/'); + } catch (error) {} }; const validateForm = () => { @@ -37,17 +39,20 @@ export default function Login() { return Object.keys(newErrors).length === 0; }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); setSubmitted(true); - console.log(validateForm()); if (validateForm()) { console.log('Form submitted successfully!'); console.log('Username:', username); console.log('Password:', password); - - dispatch(loginThunk({ username, password })); + try { + await dispatch(loginThunk({ username, password })).unwrap(); + navigate('/'); + } catch (error) { + console.error('Login failed:', error); + } } }; return ( @@ -60,6 +65,7 @@ export default function Login() { >
{ console.log('signup page handleCheckAuth'); - await dispatch(checkAuthThunk()).unwrap(); - if (user) navigate('/'); + try { + await dispatch(checkAuthThunk()).unwrap(); + if (user) navigate('/'); + } catch (error) {} }; 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)) { newErrors.email = 'Invalid email address'; } - + if (!password) { newErrors.password = 'Password is required'; } else if (password.length < 6) { @@ -45,7 +47,7 @@ export default function Signup() { return Object.keys(newErrors).length === 0; }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); setSubmitted(true); console.log(validateForm()); @@ -55,8 +57,12 @@ export default function Signup() { console.log('Username:', username); console.log('Email:', email); console.log('Password:', password); - - dispatch(signupThunk({ username, password, email })); + try { + await dispatch(signupThunk({ username, password, email })).unwrap(); + navigate('/'); + } catch (error) { + console.error('Signup failed:', error); + } } }; diff --git a/src/router/router.jsx b/src/router/router.jsx index dff99e8..39664b6 100644 --- a/src/router/router.jsx +++ b/src/router/router.jsx @@ -2,14 +2,15 @@ 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 Loading from '../components/Loading'; -import Login from '../pages/auth/Login'; -import Signup from '../pages/auth/Signup'; +import Login from '../pages/Auth/Login'; +import Signup from '../pages/Auth/Signup'; import ProtectedRoute from './protectedRoute'; import { useSelector } from 'react-redux'; function AppRouter() { - const { id } = useSelector((state) => state.auth); + const { user } = useSelector((state) => state.auth); return ( }> @@ -23,10 +24,26 @@ function AppRouter() { } /> + + + + } + /> + + + + } + /> } /> } /> - } /> + } /> ); diff --git a/src/services/api.js b/src/services/api.js index 1f003dc..7f69131 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,14 +1,22 @@ import axios from 'axios'; +import CryptoJS from 'crypto-js'; + +const secretKey = import.meta.env.VITE_SECRETKEY; // Create Axios instance with base URL const api = axios.create({ - baseURL: 'api', + baseURL: '/api', withCredentials: true, // Include cookies if needed }); // Request Interceptor api.interceptors.request.use( (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; }, (error) => { @@ -26,8 +34,8 @@ api.interceptors.response.use( // Handle errors in the response if (error.response) { // monitor /verify - if (error.response.status === 401 && error.config.url === '/check-token') { - if (window.location.pathname !== '/login' && window.location.pathname !== '/register') { + if (error.response.status === 401 && error.config.url === '/check-token/') { + if (window.location.pathname !== '/login' && window.location.pathname !== '/signup') { window.location.href = '/login'; } } @@ -58,7 +66,7 @@ const get = async (url, params = {}) => { const post = async (url, data, isMultipart = false) => { const headers = isMultipart ? { '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 }); return res.data; diff --git a/src/store/auth/auth.slice.js b/src/store/auth/auth.slice.js index 7c16ef7..615a97a 100644 --- a/src/store/auth/auth.slice.js +++ b/src/store/auth/auth.slice.js @@ -20,7 +20,7 @@ const setRejected = (state, action) => { const authSlice = createSlice({ name: 'auth', - initialState: { loading: false, error: null, user: {id: 123, username: 'test'} }, + initialState: { loading: false, error: null, user: null }, reducers: { login: (state, action) => { state.user = action.payload; diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js index 331d6b1..a29ace2 100644 --- a/src/store/auth/auth.thunk.js +++ b/src/store/auth/auth.thunk.js @@ -2,15 +2,22 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { get, post } from '../../services/api'; import { showNotification } from '../notification.slice'; import { logout } from './auth.slice'; +import CryptoJS from 'crypto-js'; + +const secretKey = import.meta.env.VITE_SECRETKEY; export const loginThunk = createAsyncThunk( 'auth/login', async ({ username, password }, { rejectWithValue, dispatch }) => { try { - const { message, user } = await post('/login', { username, password }); + const { message, user, token } = await post('/login/', { username, password }); if (!user) { throw new Error(message || 'Something went wrong'); } + // encrypt token + const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString(); + sessionStorage.setItem('token', encryptedToken); + return user; } catch (error) { 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 }) => { try { - const { user, message } = await get('/check-token'); + const { user, message } = await get('/check-token/'); if (!user) { dispatch(logout()); 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 }) => { try { // Send the logout request to the server (this assumes your server clears any session-related info) - await post('/logout'); + await post('/logout/'); dispatch(logout()); } catch (error) { const errorMessage = error.response?.data?.message || 'Log out failed'; diff --git a/src/styles/style.scss b/src/styles/style.scss index e652e38..898fcb5 100644 --- a/src/styles/style.scss +++ b/src/styles/style.scss @@ -1,5 +1,8 @@ @import 'bootstrap/scss/bootstrap'; +#root { + min-width: 24rem; +} .dropdown-toggle { outline: 0; } @@ -9,6 +12,9 @@ } .knowledge-card { + min-width: 20rem; + cursor: pointer; + .hoverdown:hover .hoverdown-menu{ display: block; color: red; diff --git a/vite.config.js b/vite.config.js index 90cdbfd..2e6c3c9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,13 +1,23 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], - build: { - outDir: '../dist' - }, - server: { - port: 8080 - } -}) +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + + return { + plugins: [react()], + build: { + outDir: '../dist', + }, + server: { + port: env.VITE_PORT, + proxy: { + '/api': { + target: env.VITE_API_URL || 'http://124.222.236.141:58000', + changeOrigin: true, + }, + }, + }, + }; +}); From 6d0ebf169c7741a472f498d46f04c5838dcf2c55 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Mon, 3 Mar 2025 19:37:47 -0500 Subject: [PATCH 05/21] [dev]add request --- src/icons/icons.js | 21 ++ src/pages/KnowledgeBase/Detail/DatasetTab.jsx | 15 +- .../KnowledgeBase/Detail/SettingsTab.jsx | 33 +-- src/pages/KnowledgeBase/KnowledgeBase.jsx | 275 +++++++++++++++++- .../KnowledgeBase/KnowledgeBaseDetail.jsx | 8 +- src/pages/KnowledgeBase/KnowledgeCard.jsx | 23 +- 6 files changed, 326 insertions(+), 49 deletions(-) diff --git a/src/icons/icons.js b/src/icons/icons.js index 8aee1f3..0305393 100644 --- a/src/icons/icons.js +++ b/src/icons/icons.js @@ -78,4 +78,25 @@ export const icons = { > `, edit: ``, + list: ` + + `, + 'setting-fill': ``, + dataset: ``, + calendar: ``, + clipboard: ``, + chat: ``, }; diff --git a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx index 986ccd1..f23b1f3 100644 --- a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx +++ b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx @@ -91,7 +91,12 @@ export default function DatasetTab({ knowledgeBase }) {
  • - {knowledgeBase.title} + + {knowledgeBase.title} +
  • 数据集 @@ -194,14 +199,14 @@ export default function DatasetTab({ knowledgeBase }) { {doc.size} {doc.updatedAt} -
    - - -
    diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx index 114c98e..fd28adf 100644 --- a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -2,21 +2,6 @@ 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 ( - (e.currentTarget.style.color = '#212529')} - onMouseOut={(e) => (e.currentTarget.style.color = '')} - > - {children} - - ); -}; - export default function SettingsTab({ knowledgeBase }) { // Handle form submission const handleSubmit = (e) => { @@ -38,12 +23,12 @@ export default function SettingsTab({ knowledgeBase }) {
  • - @@ -177,7 +162,7 @@ export default function SettingsTab({ knowledgeBase }) { -
    diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index d789dec..c31631b 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -9,6 +9,17 @@ export default function KnowledgeBase() { const dispatch = useDispatch(); const navigate = useNavigate(); const [showCreateModal, setShowCreateModal] = useState(false); + const [showAccessRequestModal, setShowAccessRequestModal] = useState(false); + const [formErrors, setFormErrors] = useState({}); + const [accessRequestErrors, setAccessRequestErrors] = useState({}); + const [accessRequestData, setAccessRequestData] = useState({ + id: '', + title: '', + accessType: '只读访问', + duration: '一周', + projectInfo: '', + reason: '', + }); const [newKnowledgeBase, setNewKnowledgeBase] = useState({ title: '', description: '', @@ -47,17 +58,65 @@ export default function KnowledgeBase() { ...prev, [name]: value, })); + + // Clear error when user types + if (formErrors[name]) { + setFormErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + const handleAccessRequestInputChange = (e) => { + const { name, value } = e.target; + setAccessRequestData((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error when user types + if (accessRequestErrors[name]) { + setAccessRequestErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + const validateCreateForm = () => { + const errors = {}; + + if (!newKnowledgeBase.title.trim()) { + errors.title = '请输入知识库名称'; + } + + if (!newKnowledgeBase.description.trim()) { + errors.description = '请输入知识库描述'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + const validateAccessRequestForm = () => { + const errors = {}; + + if (!accessRequestData.projectInfo.trim()) { + errors.projectInfo = '请输入项目信息'; + } + + if (!accessRequestData.reason.trim()) { + errors.reason = '请输入申请原因'; + } + + setAccessRequestErrors(errors); + return Object.keys(errors).length === 0; }; const handleCreateKnowledgeBase = () => { - // Here you would typically call an API to create the knowledge base - if (!newKnowledgeBase.title.trim()) { - dispatch( - showNotification({ - message: '请输入知识库名称', - type: 'error', - }) - ); + // Validate form + if (!validateCreateForm()) { return; } @@ -75,6 +134,7 @@ export default function KnowledgeBase() { // Reset form and close modal setNewKnowledgeBase({ title: '', description: '' }); + setFormErrors({}); setShowCreateModal(false); // Navigate to the newly created knowledge base with datasets tab @@ -85,6 +145,40 @@ export default function KnowledgeBase() { navigate(`/knowledge-base/${id}/datasets`); }; + const handleRequestAccess = (id, title) => { + setAccessRequestData((prev) => ({ + ...prev, + id, + title, + })); + setAccessRequestErrors({}); + setShowAccessRequestModal(true); + }; + + const handleSubmitAccessRequest = () => { + // Validate form + if (!validateAccessRequestForm()) { + return; + } + + // Here you would typically call an API to submit the access request + dispatch( + showNotification({ + message: '权限申请已提交', + type: 'success', + }) + ); + + // Reset form and close modal + setAccessRequestData((prev) => ({ + ...prev, + projectInfo: '', + reason: '', + })); + setAccessRequestErrors({}); + setShowAccessRequestModal(false); + }; + return (
    @@ -101,7 +195,11 @@ export default function KnowledgeBase() {
    {knowledgeList.map((item) => ( - handleCardClick(item.id)} /> + handleCardClick(item.id)} + onRequestAccess={handleRequestAccess} + /> ))}
    @@ -143,11 +241,11 @@ export default function KnowledgeBase() {
    + {formErrors.title &&
    {formErrors.title}
    }
    + {formErrors.description && ( +
    {formErrors.description}
    + )}
    @@ -186,6 +289,152 @@ export default function KnowledgeBase() {
    )} + + {/* 申请访问权限弹窗 */} + {showAccessRequestModal && ( +
    +
    +
    +
    申请访问权限
    + +
    +
    +
    + +
    +
    + setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' })) + } + > +
    只读访问
    +
    仅查看数据集内容
    +
    +
    + setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' })) + } + > +
    完全访问
    +
    查看、编辑和管理数据
    +
    +
    +
    + +
    + + +
    + +
    + + + {accessRequestErrors.projectInfo && ( +
    {accessRequestErrors.projectInfo}
    + )} +
    + +
    + + + {accessRequestErrors.reason && ( +
    {accessRequestErrors.reason}
    + )} +
    +
    +
    + + +
    +
    +
    + )}
    ); } diff --git a/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx index 5c0b033..d1a3b28 100644 --- a/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx @@ -45,7 +45,7 @@ export default function KnowledgeBaseDetail() { diff --git a/src/pages/KnowledgeBase/KnowledgeCard.jsx b/src/pages/KnowledgeBase/KnowledgeCard.jsx index bf0f0d0..6573059 100644 --- a/src/pages/KnowledgeBase/KnowledgeCard.jsx +++ b/src/pages/KnowledgeBase/KnowledgeCard.jsx @@ -1,7 +1,18 @@ import React from 'react'; import SvgIcon from '../../components/SvgIcon'; -export default function KnowledgeCard({ id, title, description, documents, date, access, onClick }) { +export default function KnowledgeCard({ id, title, description, documents, date, access, onClick, onRequestAccess }) { + const handleNewChat = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleRequestAccess = (e) => { + e.preventDefault(); + e.stopPropagation(); + onRequestAccess(id, title); + }; + return (
    @@ -44,12 +55,18 @@ export default function KnowledgeCard({ id, title, description, documents, date, )} {access === 'full' || access === 'read' ? ( - ) : ( - From 546f4c4265304202ead518fabfefb8ca59f92b92 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Tue, 4 Mar 2025 14:46:45 -0500 Subject: [PATCH 06/21] [dev]chat pages --- src/components/SvgIcon.jsx | 40 +- src/icons/icons.js | 6 +- src/layouts/HeaderWithNav.jsx | 20 +- src/pages/Chat/Chat.jsx | 67 +++ src/pages/Chat/ChatSidebar.jsx | 94 ++++ src/pages/Chat/ChatWindow.jsx | 161 ++++++ src/pages/Chat/NewChat.jsx | 102 ++++ src/pages/KnowledgeBase/Detail/DatasetTab.jsx | 331 +++++++++++-- .../KnowledgeBase/Detail/SettingsTab.jsx | 458 ++++++++++++++++-- src/pages/KnowledgeBase/KnowledgeBase.jsx | 2 +- src/pages/KnowledgeBase/KnowledgeCard.jsx | 4 + src/router/router.jsx | 25 + 12 files changed, 1223 insertions(+), 87 deletions(-) create mode 100644 src/pages/Chat/Chat.jsx create mode 100644 src/pages/Chat/ChatSidebar.jsx create mode 100644 src/pages/Chat/ChatWindow.jsx create mode 100644 src/pages/Chat/NewChat.jsx diff --git a/src/components/SvgIcon.jsx b/src/components/SvgIcon.jsx index 4a71d99..61d2b0c 100644 --- a/src/components/SvgIcon.jsx +++ b/src/components/SvgIcon.jsx @@ -1,12 +1,46 @@ import React from 'react'; import { icons } from '../icons/icons'; -export default function SvgIcon({ className }) { +export default function SvgIcon({ className, width, height, color, style }) { + // Create a new SVG string with custom attributes if provided + const customizeSvg = (svgString) => { + if (!svgString) return ''; + + // If no customization needed, return the original SVG + if (!width && !height && !color) return svgString; + + // Parse the SVG to modify attributes + let modifiedSvg = svgString; + + // Replace width if provided + if (width) { + modifiedSvg = modifiedSvg.replace(/width=['"]([^'"]*)['"]/g, `width="${width}"`); + } + + // Replace height if provided + if (height) { + modifiedSvg = modifiedSvg.replace(/height=['"]([^'"]*)['"]/g, `height="${height}"`); + } + + // Replace fill color if provided + if (color) { + modifiedSvg = modifiedSvg.replace(/fill=['"]currentColor['"]/g, `fill="${color}"`); + } + + return modifiedSvg; + }; + return ( ); } diff --git a/src/icons/icons.js b/src/icons/icons.js index 0305393..a5424d5 100644 --- a/src/icons/icons.js +++ b/src/icons/icons.js @@ -94,9 +94,11 @@ export const icons = { p-id='21341' > `, - 'setting-fill': ``, - dataset: ``, + 'setting-fill': ``, + dataset: ``, calendar: ``, clipboard: ``, chat: ``, + 'arrowup-upload': ``, + send: ``, }; diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx index 8a290ba..3ee1685 100644 --- a/src/layouts/HeaderWithNav.jsx +++ b/src/layouts/HeaderWithNav.jsx @@ -1,11 +1,12 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; import { logoutThunk } from '../store/auth/auth.thunk'; export default function HeaderWithNav() { const dispatch = useDispatch(); const navigate = useNavigate(); + const location = useLocation(); const { user } = useSelector((state) => state.auth); const handleLogout = async () => { @@ -16,9 +17,14 @@ export default function HeaderWithNav() { } catch (error) {} }; + // Check if the current path starts with the given path + const isActive = (path) => { + return location.pathname.startsWith(path); + }; + return ( -
    -
    @@ -201,13 +352,14 @@ export default function DatasetTab({ knowledgeBase }) {
    - -
    @@ -222,7 +374,7 @@ export default function DatasetTab({ knowledgeBase }) {
    每页行数: - @@ -231,7 +383,7 @@ export default function DatasetTab({ knowledgeBase }) {
    1-5 of 10
    + + {/* Add File Modal */} + {showAddFileModal && ( +
    +
    +
    +
    新增文件
    + +
    +
    +
    + + + 文件名称将自动填充为上传文件的名称 +
    +
    + + +
    +
    + +
    + + {newFile.file ? ( +
    +

    {newFile.name}

    +

    + {(newFile.file.size / 1024).toFixed(2)} KB +

    +
    + ) : ( +
    + +

    点击或拖拽文件到此处上传

    +

    支持 PDF, DOCX, TXT, CSV 等格式

    +
    + )} +
    + {fileErrors.file &&
    {fileErrors.file}
    } +
    +
    +
    + + +
    +
    +
    + )} ); } diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx index fd28adf..f08a19e 100644 --- a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -1,8 +1,114 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import SvgIcon from '../../../components/SvgIcon'; +import { useDispatch } from 'react-redux'; +import { showNotification } from '../../../store/notification.slice'; export default function SettingsTab({ 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); + const [formErrors, setFormErrors] = useState({}); + + // 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 form submission const handleSubmit = (e) => { e.preventDefault(); @@ -16,6 +122,83 @@ export default function SettingsTab({ knowledgeBase }) { console.log('Deleting knowledge base:', knowledgeBase.id); }; + // 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 */} @@ -23,10 +206,17 @@ export default function SettingsTab({ knowledgeBase }) {
    + + {/* Pagination */} + {users.length > usersPerPage && ( +
    + +
    + )} +
    @@ -169,6 +402,143 @@ export default function SettingsTab({ knowledgeBase }) {
    + + {/* Edit User Permissions Modal */} + {showEditModal && ( +
    +
    +
    +
    编辑用户权限
    + +
    +
    +
    + +
    +
    +
    + ID: + #{editUser?.id} +
    +
    + 用户名: + {editUser?.username} +
    +
    + 邮箱: + {editUser?.email} +
    +
    +
    +
    + +
    + +
    +
    + handleEditInputChange({ target: { name: 'permissionType', value: '只读' } }) + } + > +
    只读
    +
    仅查看数据集内容
    +
    +
    + handleEditInputChange({ + target: { name: 'permissionType', value: '完全访问' }, + }) + } + > +
    完全访问
    +
    查看、编辑和管理数据
    +
    +
    + {formErrors.permissionType && ( +
    {formErrors.permissionType}
    + )} +
    + +
    + + + {formErrors.accessDuration && ( +
    {formErrors.accessDuration}
    + )} +
    +
    +
    + + +
    +
    +
    + )} ); } diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index c31631b..389a3a6 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -180,7 +180,7 @@ export default function KnowledgeBase() { }; return ( -
    +
    - {showBatchDropdown && ( -
      -
    • - -
    • -
    - )} -
    -
    - setSearchQuery(e.target.value)} - /> - - - -
    -
    - -
    - - {/* Documents table */} -
    -
    - - - - - - - - - - - - - - {documents.map((doc) => ( - - - - - - - - - - ))} - -
    -
    - -
    -
    ID名称描述文档大小更新日期操作
    -
    - handleSelectDocument(doc.id)} - /> -
    -
    #{doc.id}{doc.name}{doc.description}{doc.size}{doc.updatedAt} -
    - - -
    -
    -
    -
    - - {/* Pagination */} -
    -
    - 每页行数: - -
    -
    - 1-5 of 10 - -
    -
    - - {/* Add File Modal */} - {showAddFileModal && ( -
    -
    + + {selectedDocuments.length > 0 && ( +
    -
    -
    -
    - - - 文件名称将自动填充为上传文件的名称 -
    -
    - - -
    -
    - -
    - - {newFile.file ? ( -
    -

    {newFile.name}

    -

    - {(newFile.file.size / 1024).toFixed(2)} KB -

    -
    - ) : ( -
    - -

    点击或拖拽文件到此处上传

    -

    支持 PDF, DOCX, TXT, CSV 等格式

    -
    - )} -
    - {fileErrors.file &&
    {fileErrors.file}
    } -
    -
    -
    - - + {showBatchDropdown && ( +
      +
    • + +
    • +
    + )}
    -
    + )}
    - )} +
    + +
    +
    + + {/* Document list */} + + + {/* File upload modal */} + ); } diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx index f08a19e..0b2705f 100644 --- a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -1,11 +1,27 @@ import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; -import SvgIcon from '../../../components/SvgIcon'; +import { useNavigate } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { showNotification } from '../../../store/notification.slice'; +import { updateKnowledgeBase, deleteKnowledgeBase } from '../../../store/knowledgeBase/knowledgeBase.thunks'; + +// 导入拆分的组件 +import Breadcrumb from './components/Breadcrumb'; +import KnowledgeBaseForm from './components/KnowledgeBaseForm'; +import DeleteConfirmModal from './components/DeleteConfirmModal'; export default function SettingsTab({ knowledgeBase }) { const dispatch = useDispatch(); + const navigate = useNavigate(); + + // State for knowledge base form + const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({ + name: knowledgeBase.name, + desc: knowledgeBase.desc, + }); + 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; @@ -13,7 +29,6 @@ export default function SettingsTab({ knowledgeBase }) { // State for edit modal const [showEditModal, setShowEditModal] = useState(false); const [editUser, setEditUser] = useState(null); - const [formErrors, setFormErrors] = useState({}); // Mock data for users with permissions - convert to state so we can update it const [users, setUsers] = useState([ @@ -109,17 +124,108 @@ export default function SettingsTab({ knowledgeBase }) { 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; + setKnowledgeBaseForm((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error if exists + if (formErrors[name]) { + setFormErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + // Validate knowledge base form + const validateForm = () => { + const errors = {}; + + if (!knowledgeBaseForm.name.trim()) { + errors.name = '请输入知识库名称'; + } + + if (!knowledgeBaseForm.desc.trim()) { + errors.desc = '请输入知识库描述'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + // 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'); + + // Validate form + if (!validateForm()) { + return; + } + + setIsSubmitting(true); + + // Dispatch update knowledge base action + dispatch( + updateKnowledgeBase({ + id: knowledgeBase.id, + data: { + name: knowledgeBaseForm.name, + desc: knowledgeBaseForm.desc, + }, + }) + ) + .unwrap() + .then(() => { + dispatch( + showNotification({ + message: '知识库更新成功', + type: 'success', + }) + ); + setIsSubmitting(false); + }) + .catch((error) => { + dispatch( + showNotification({ + message: `更新失败: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + setIsSubmitting(false); + }); }; // 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); + setIsSubmitting(true); + + // Dispatch delete knowledge base action + dispatch(deleteKnowledgeBase(knowledgeBase.id)) + .unwrap() + .then(() => { + dispatch( + showNotification({ + message: '知识库已删除', + type: 'success', + }) + ); + // Navigate back to knowledge base list + navigate('/knowledge-base'); + }) + .catch((error) => { + dispatch( + showNotification({ + message: `删除失败: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + setIsSubmitting(false); + setShowDeleteConfirm(false); + }); }; // Handle edit user permissions @@ -202,206 +308,27 @@ export default function SettingsTab({ knowledgeBase }) { return ( <> {/* Breadcrumb navigation */} -
    - -
    + - {/* Settings form */} -
    -
    -
    知识库设置
    + {/* Knowledge Base Form */} + setShowDeleteConfirm(true)} + /> -
    -
    - - -
    - -
    - - -
    - -
    - -
    - - -
    -
    - - -
    -
    - -
    - -
    -
    - - - - - - - - - - - - - {currentUsers.map((user) => ( - - - - - - - - - ))} - -
    ID用户名邮箱权限类型访问时长操作
    #{user.id}{user.username}{user.email} - - {user.permissionType} - - {user.accessDuration} -
    - - -
    -
    -
    -
    - - {/* Pagination */} - {users.length > usersPerPage && ( -
    - -
    - )} - - -
    - -
    - - -
    -
    -
    -
    + {/* Delete confirmation modal */} + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + /> {/* Edit User Permissions Modal */} {showEditModal && ( diff --git a/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx new file mode 100644 index 0000000..4d3b6ec --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +/** + * 面包屑导航组件 + */ +const Breadcrumb = ({ knowledgeBase, activeTab }) => { + return ( +
    + +
    + ); +}; + +export default Breadcrumb; diff --git a/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx new file mode 100644 index 0000000..e8550f1 --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx @@ -0,0 +1,70 @@ +import React from 'react'; + +/** + * 删除确认模态框组件 + */ +const DeleteConfirmModal = ({ show, title, isSubmitting, onCancel, onConfirm }) => { + if (!show) return null; + + return ( +
    +
    +
    +
    确认删除
    + +
    +
    +

    您确定要删除知识库 "{title}" 吗?此操作不可撤销。

    +
    +
    + + +
    +
    +
    + ); +}; + +export default DeleteConfirmModal; diff --git a/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx new file mode 100644 index 0000000..747832a --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import SvgIcon from '../../../../components/SvgIcon'; + +/** + * 文档列表组件 + */ +const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocument, onDeleteDocument, selectAll }) => { + if (documents.length === 0) { + return
    暂无数据集,请上传数据集
    ; + } + + return ( +
    + + + + + + + + + + + + + {documents.map((doc) => ( + + + + + + + + + ))} + +
    +
    + +
    +
    名称描述大小更新时间 + 操作 +
    +
    + onSelectDocument(doc.id)} + /> +
    +
    {doc.name}{doc.description}{doc.size}{new Date(doc.update_time).toLocaleDateString()} +
    + +
    +
    +
    + ); +}; + +export default DocumentList; diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx new file mode 100644 index 0000000..0ccbb0e --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx @@ -0,0 +1,118 @@ +import React, { useRef } from 'react'; + +/** + * 文件上传模态框组件 + */ +const FileUploadModal = ({ + show, + newFile, + fileErrors, + isSubmitting, + onClose, + onDescriptionChange, + onFileChange, + onFileDrop, + onDragOver, + onUploadAreaClick, + onUpload, +}) => { + const fileInputRef = useRef(null); + + if (!show) return null; + + return ( +
    +
    +
    +
    上传文件
    + +
    +
    +
    + + {newFile.file ? ( +
    +

    已选择文件:

    +

    {newFile.file.name}

    +
    + ) : ( +
    +

    点击或拖拽文件到此处上传

    +

    支持 PDF, DOCX, TXT, CSV 等格式

    +
    + )} + {fileErrors.file &&
    {fileErrors.file}
    } +
    +
    + + +
    +
    +
    + + +
    +
    +
    + ); +}; + +export default FileUploadModal; diff --git a/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx new file mode 100644 index 0000000..761ee0d --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx @@ -0,0 +1,116 @@ +import React from 'react'; + +/** + * 知识库表单组件 + */ +const KnowledgeBaseForm = ({ + formData, + formErrors, + isSubmitting, + knowledgeBase, + onInputChange, + onSubmit, + onDelete, +}) => { + return ( +
    +
    +
    知识库设置
    + +
    +
    + + + {formErrors.name &&
    {formErrors.name}
    } +
    + +
    + + + {formErrors.desc &&
    {formErrors.desc}
    } +
    + +
    + +
    + + +
    +
    + + +
    +
    + +
    + +

    {new Date(knowledgeBase.create_time).toLocaleString()}

    +
    + +
    + +

    {new Date(knowledgeBase.update_time).toLocaleString()}

    +
    + +
    + + +
    +
    +
    +
    + ); +}; + +export default KnowledgeBaseForm; diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index 389a3a6..a0e65c0 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -1,10 +1,21 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import KnowledgeCard from './KnowledgeCard'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { showNotification } from '../../store/notification.slice'; +import { + fetchKnowledgeBases, + searchKnowledgeBases, + createKnowledgeBase, +} from '../../store/knowledgeBase/knowledgeBase.thunks'; +import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice'; import SvgIcon from '../../components/SvgIcon'; +// 导入拆分的组件 +import SearchBar from './components/SearchBar'; +import Pagination from './components/Pagination'; +import CreateKnowledgeBaseModal from './components/CreateKnowledgeBaseModal'; +import KnowledgeBaseList from './components/KnowledgeBaseList'; + export default function KnowledgeBase() { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -21,36 +32,146 @@ export default function KnowledgeBase() { reason: '', }); const [newKnowledgeBase, setNewKnowledgeBase] = useState({ - title: '', - description: '', + name: '', + desc: '', }); - const knowledgeList = [ - { - id: '1', - title: '产品开发知识库', - description: '产品开发流程及规范说明文档', - documents: 24, - date: '2025-02-15', - access: 'full', - }, - { - id: '2', - title: '市场分析知识库', - description: '2025年Q1市场分析总结', - documents: 12, - date: '2025-02-10', - access: 'read', - }, - { - id: '3', - title: '财务知识库', - description: '月度财务分析报告', - documents: 8, - date: '2025-02-01', - access: 'none', - }, - ]; + // Search state + const [searchKeyword, setSearchKeyword] = useState(''); + const [isSearching, setIsSearching] = useState(false); + + // Pagination state + const [pagination, setPagination] = useState({ + page: 1, + page_size: 10, + }); + + // Get knowledge bases from Redux store + const { items: knowledgeBases, total, status, error } = useSelector((state) => state.knowledgeBase.list); + const { + items: searchResults, + total: searchTotal, + status: searchStatus, + error: searchError, + keyword: storeKeyword, + } = useSelector((state) => state.knowledgeBase.search); + const { status: operationStatus, error: operationError } = useSelector((state) => state.knowledgeBase.operations); + + // Determine which data to display based on search state + const displayData = isSearching ? searchResults : knowledgeBases; + const displayTotal = isSearching ? searchTotal : total; + const displayStatus = isSearching ? searchStatus : status; + const displayError = isSearching ? searchError : error; + + // Fetch knowledge bases when component mounts or pagination changes + useEffect(() => { + if (!isSearching) { + dispatch(fetchKnowledgeBases(pagination)); + } else if (searchKeyword.trim()) { + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: pagination.page, + page_size: pagination.page_size, + }) + ); + } + }, [dispatch, pagination.page, pagination.page_size, isSearching, searchKeyword]); + + // Handle search input change + const handleSearchInputChange = (e) => { + setSearchKeyword(e.target.value); + }; + + // Handle search submit + const handleSearch = (e) => { + e.preventDefault(); + + if (searchKeyword.trim()) { + setIsSearching(true); + setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: 1, + page_size: pagination.page_size, + }) + ); + } else { + // If search is empty, reset to normal list view + handleClearSearch(); + } + }; + + // Handle clear search + const handleClearSearch = () => { + setSearchKeyword(''); + setIsSearching(false); + setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page + dispatch(resetSearchState()); + }; + + // Show loading state while fetching data + const isLoading = displayStatus === 'loading'; + + // Show error notification if fetch fails + useEffect(() => { + if (displayStatus === 'failed' && displayError) { + dispatch( + showNotification({ + message: `获取知识库列表失败: ${displayError.message || displayError}`, + type: 'danger', + }) + ); + } + }, [displayStatus, displayError, dispatch]); + + // Show notification for operation status + useEffect(() => { + if (operationStatus === 'succeeded') { + dispatch( + showNotification({ + message: '操作成功', + type: 'success', + }) + ); + // Refresh the list after successful operation + if (isSearching && searchKeyword.trim()) { + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: pagination.page, + page_size: pagination.page_size, + }) + ); + } else { + dispatch(fetchKnowledgeBases(pagination)); + } + } else if (operationStatus === 'failed' && operationError) { + dispatch( + showNotification({ + message: `操作失败: ${operationError.message || operationError}`, + type: 'danger', + }) + ); + } + }, [operationStatus, operationError, dispatch, pagination, isSearching, searchKeyword]); + + // Handle pagination change + const handlePageChange = (newPage) => { + setPagination((prev) => ({ + ...prev, + page: newPage, + })); + }; + + // Handle page size change + const handlePageSizeChange = (newPageSize) => { + setPagination({ + page: 1, // Reset to first page when changing page size + page_size: newPageSize, + }); + }; const handleInputChange = (e) => { const { name, value } = e.target; @@ -87,12 +208,12 @@ export default function KnowledgeBase() { const validateCreateForm = () => { const errors = {}; - if (!newKnowledgeBase.title.trim()) { - errors.title = '请输入知识库名称'; + if (!newKnowledgeBase.name.trim()) { + errors.name = '请输入知识库名称'; } - if (!newKnowledgeBase.description.trim()) { - errors.description = '请输入知识库描述'; + if (!newKnowledgeBase.desc.trim()) { + errors.desc = '请输入知识库描述'; } setFormErrors(errors); @@ -120,27 +241,22 @@ export default function KnowledgeBase() { return; } - // For now, just show a success notification + // Dispatch create knowledge base action dispatch( - showNotification({ - message: '知识库创建成功', - type: 'success', + createKnowledgeBase({ + name: newKnowledgeBase.name, + desc: newKnowledgeBase.desc, + type: 'private', // Default type }) ); - // 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: '' }); + setNewKnowledgeBase({ name: '', desc: '' }); setFormErrors({}); setShowCreateModal(false); - - // Navigate to the newly created knowledge base with datasets tab - navigate(`/knowledge-base/${newId}/datasets`); }; + // Handle card click to navigate to knowledge base detail const handleCardClick = (id) => { navigate(`/knowledge-base/${id}/datasets`); }; @@ -179,10 +295,19 @@ export default function KnowledgeBase() { setShowAccessRequestModal(false); }; + // Calculate total pages + const totalPages = Math.ceil(displayTotal / pagination.page_size); + return (
    - +
    -
    - {knowledgeList.map((item) => ( - - handleCardClick(item.id)} - onRequestAccess={handleRequestAccess} - /> - - ))} -
    - - {/* 新建知识库弹窗 */} - {showCreateModal && ( -
    -
    -
    -
    新建知识库
    - -
    -
    -
    - - - {formErrors.title &&
    {formErrors.title}
    } -
    -
    - - - {formErrors.description && ( -
    {formErrors.description}
    - )} -
    -
    -
    - - -
    -
    + {isSearching && ( +
    + 搜索结果: "{storeKeyword}" - 找到 {displayTotal} 个知识库
    )} - {/* 申请访问权限弹窗 */} + {isLoading ? ( +
    +
    + 加载中... +
    +
    + ) : ( + <> + + + {/* Pagination */} + {totalPages > 1 && ( + + )} + + )} + + {/* 新建知识库弹窗 */} + setShowCreateModal(false)} + onChange={handleInputChange} + onSubmit={handleCreateKnowledgeBase} + /> + + {/* 申请权限弹窗 */} {showAccessRequestModal && (
    -
    - -
    -
    - setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' })) - } - > -
    只读访问
    -
    仅查看数据集内容
    -
    -
    - setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' })) - } - > -
    完全访问
    -
    查看、编辑和管理数据
    -
    -
    -
    -
    - + + +
    +
    + + +
    +
    +
    -
    -
    -
    -
    -
    +
    -
    diff --git a/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx index d1a3b28..651820c 100644 --- a/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBaseDetail.jsx @@ -1,5 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { showNotification } from '../../store/notification.slice'; import SvgIcon from '../../components/SvgIcon'; import DatasetTab from './Detail/DatasetTab'; import SettingsTab from './Detail/SettingsTab'; @@ -7,8 +9,13 @@ import SettingsTab from './Detail/SettingsTab'; export default function KnowledgeBaseDetail() { const { id, tab } = useParams(); const navigate = useNavigate(); + const dispatch = useDispatch(); const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets'); + // Get knowledge base details from Redux store + const { items: knowledgeBases } = useSelector((state) => state.knowledgeBase.list); + const knowledgeBase = knowledgeBases.find((kb) => kb.id === id); + // Update active tab when URL changes useEffect(() => { if (tab) { @@ -16,15 +23,18 @@ export default function KnowledgeBaseDetail() { } }, [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, - }; + // If knowledge base not found in Redux store, show notification and redirect + useEffect(() => { + if (!knowledgeBase && knowledgeBases.length > 0) { + dispatch( + showNotification({ + message: '未找到知识库,请返回知识库列表', + type: 'warning', + }) + ); + navigate('/knowledge-base'); + } + }, [knowledgeBase, knowledgeBases, dispatch, navigate]); // Handle tab change const handleTabChange = (tab) => { @@ -32,14 +42,25 @@ export default function KnowledgeBaseDetail() { navigate(`/knowledge-base/${id}/${tab}`); }; + // Show loading state if knowledge base not loaded yet + if (!knowledgeBase) { + return ( +
    +
    + 加载中... +
    +
    + ); + } + return (
    {/* Sidebar */}
    -
    {knowledgeBase.title}
    -

    {knowledgeBase.description}

    +
    {knowledgeBase.name}
    +

    {knowledgeBase.desc}


    diff --git a/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx b/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx new file mode 100644 index 0000000..c5af8f4 --- /dev/null +++ b/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx @@ -0,0 +1,92 @@ +import React from 'react'; +import SvgIcon from '../../../components/SvgIcon'; + +/** + * 创建知识库模态框组件 + */ +const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => { + if (!show) return null; + + return ( +
    +
    +
    +
    新建知识库
    + +
    +
    +
    + + + {formErrors.name &&
    {formErrors.name}
    } +
    +
    + + + {formErrors.desc &&
    {formErrors.desc}
    } +
    +
    +
    + + +
    +
    +
    + ); +}; + +export default CreateKnowledgeBaseModal; diff --git a/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx new file mode 100644 index 0000000..eb042e5 --- /dev/null +++ b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import KnowledgeCard from '../KnowledgeCard'; + +/** + * 知识库列表组件 + */ +const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequestAccess }) => { + if (knowledgeBases.length === 0) { + return ( +
    + {isSearching ? '没有找到匹配的知识库' : '暂无知识库,请创建新的知识库'} +
    + ); + } + + return ( +
    + {knowledgeBases.map((item) => ( + + onCardClick(item.id)} + onRequestAccess={onRequestAccess} + /> + + ))} +
    + ); +}; + +export default KnowledgeBaseList; diff --git a/src/pages/KnowledgeBase/components/Pagination.jsx b/src/pages/KnowledgeBase/components/Pagination.jsx new file mode 100644 index 0000000..8c8f4b1 --- /dev/null +++ b/src/pages/KnowledgeBase/components/Pagination.jsx @@ -0,0 +1,53 @@ +import React from 'react'; + +/** + * 分页组件 + */ +const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => { + return ( +
    +
    + +
    + +
    + ); +}; + +export default Pagination; diff --git a/src/pages/KnowledgeBase/components/SearchBar.jsx b/src/pages/KnowledgeBase/components/SearchBar.jsx new file mode 100644 index 0000000..d20a2e6 --- /dev/null +++ b/src/pages/KnowledgeBase/components/SearchBar.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import SvgIcon from '../../../components/SvgIcon'; + +/** + * 知识库搜索栏组件 + */ +const SearchBar = ({ searchKeyword, isSearching, onSearchChange, onSearch, onClearSearch }) => { + return ( +
    + + {/* */} + {isSearching && ( + + )} +
    + ); +}; + +export default SearchBar; diff --git a/src/services/api.js b/src/services/api.js index 7f69131..80e61ea 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -34,7 +34,7 @@ api.interceptors.response.use( // Handle errors in the response if (error.response) { // monitor /verify - if (error.response.status === 401 && error.config.url === '/check-token/') { + if (error.response.status === 401 && error.config.url === '/auth/verify-token/') { if (window.location.pathname !== '/login' && window.location.pathname !== '/signup') { window.location.href = '/login'; } diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js index a29ace2..8efd5bb 100644 --- a/src/store/auth/auth.thunk.js +++ b/src/store/auth/auth.thunk.js @@ -10,15 +10,16 @@ export const loginThunk = createAsyncThunk( 'auth/login', async ({ username, password }, { rejectWithValue, dispatch }) => { try { - const { message, user, token } = await post('/login/', { username, password }); - if (!user) { + const { message, data } = await post('/auth/login/', { username, password }); + if (!data) { throw new Error(message || 'Something went wrong'); } + const { token } = data; // encrypt token const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString(); sessionStorage.setItem('token', encryptedToken); - return user; + return data; } catch (error) { const errorMessage = error.response?.data?.message || 'Something went wrong'; dispatch( @@ -51,9 +52,9 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje } }); -export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectWithValue, dispatch }) => { +export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => { try { - const { user, message } = await get('/check-token/'); + const { user, message } = await get('/auth/verify-token/'); if (!user) { dispatch(logout()); throw new Error(message || 'No token found'); @@ -69,7 +70,7 @@ export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectW export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => { try { // Send the logout request to the server (this assumes your server clears any session-related info) - await post('/logout/'); + await post('/auth/logout/'); dispatch(logout()); } catch (error) { const errorMessage = error.response?.data?.message || 'Log out failed'; diff --git a/src/store/knowledgeBase/knowledgeBase.slice.js b/src/store/knowledgeBase/knowledgeBase.slice.js new file mode 100644 index 0000000..d37445a --- /dev/null +++ b/src/store/knowledgeBase/knowledgeBase.slice.js @@ -0,0 +1,198 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + fetchKnowledgeBases, + searchKnowledgeBases, + createKnowledgeBase, + getKnowledgeBaseById, + updateKnowledgeBase, + deleteKnowledgeBase, +} from './knowledgeBase.thunks'; + +const initialState = { + // List state + list: { + items: [], + total: 0, + page: 1, + page_size: 10, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Search state + search: { + items: [], + total: 0, + page: 1, + page_size: 10, + keyword: '', + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Current knowledge base details + current: { + data: null, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Create/update/delete operations status + operations: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + operationType: null, // 'create' | 'update' | 'delete' + }, +}; + +const knowledgeBaseSlice = createSlice({ + name: 'knowledgeBase', + initialState, + reducers: { + resetOperationStatus: (state) => { + state.operations = { + status: 'idle', + error: null, + operationType: null, + }; + }, + resetCurrentKnowledgeBase: (state) => { + state.current = { + data: null, + status: 'idle', + error: null, + }; + }, + resetSearchState: (state) => { + state.search = { + items: [], + total: 0, + page: 1, + page_size: 10, + keyword: '', + status: 'idle', + error: null, + }; + }, + }, + extraReducers: (builder) => { + builder + // Fetch knowledge bases + .addCase(fetchKnowledgeBases.pending, (state) => { + state.list.status = 'loading'; + }) + .addCase(fetchKnowledgeBases.fulfilled, (state, action) => { + state.list.status = 'succeeded'; + state.list.items = action.payload.items; + state.list.total = action.payload.total; + state.list.page = action.payload.page; + state.list.page_size = action.payload.page_size; + state.list.error = null; + }) + .addCase(fetchKnowledgeBases.rejected, (state, action) => { + state.list.status = 'failed'; + state.list.error = action.payload; + }) + + // Search knowledge bases + .addCase(searchKnowledgeBases.pending, (state, action) => { + state.search.status = 'loading'; + // Store the keyword for reference + if (action.meta.arg.keyword) { + state.search.keyword = action.meta.arg.keyword; + } + }) + .addCase(searchKnowledgeBases.fulfilled, (state, action) => { + state.search.status = 'succeeded'; + state.search.items = action.payload.items; + state.search.total = action.payload.total; + state.search.page = action.payload.page; + state.search.page_size = action.payload.page_size; + state.search.error = null; + }) + .addCase(searchKnowledgeBases.rejected, (state, action) => { + state.search.status = 'failed'; + state.search.error = action.payload; + }) + + // Get knowledge base by ID + .addCase(getKnowledgeBaseById.pending, (state) => { + state.current.status = 'loading'; + }) + .addCase(getKnowledgeBaseById.fulfilled, (state, action) => { + state.current.status = 'succeeded'; + state.current.data = action.payload; + state.current.error = null; + }) + .addCase(getKnowledgeBaseById.rejected, (state, action) => { + state.current.status = 'failed'; + state.current.error = action.payload; + }) + + // Create knowledge base + .addCase(createKnowledgeBase.pending, (state) => { + state.operations.status = 'loading'; + state.operations.operationType = 'create'; + }) + .addCase(createKnowledgeBase.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // Don't add to list here - better to refetch the list to ensure consistency + state.operations.error = null; + }) + .addCase(createKnowledgeBase.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload; + }) + + // Update knowledge base + .addCase(updateKnowledgeBase.pending, (state) => { + state.operations.status = 'loading'; + state.operations.operationType = 'update'; + }) + .addCase(updateKnowledgeBase.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // Update in list if present + const index = state.list.items.findIndex((item) => item.id === action.payload.id); + if (index !== -1) { + state.list.items[index] = action.payload; + } + // Update in search results if present + const searchIndex = state.search.items.findIndex((item) => item.id === action.payload.id); + if (searchIndex !== -1) { + state.search.items[searchIndex] = action.payload; + } + // Update current if it's the same knowledge base + if (state.current.data && state.current.data.id === action.payload.id) { + state.current.data = action.payload; + } + state.operations.error = null; + }) + .addCase(updateKnowledgeBase.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload; + }) + + // Delete knowledge base + .addCase(deleteKnowledgeBase.pending, (state) => { + state.operations.status = 'loading'; + state.operations.operationType = 'delete'; + }) + .addCase(deleteKnowledgeBase.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // Remove from list if present + state.list.items = state.list.items.filter((item) => item.id !== action.payload); + // Remove from search results if present + state.search.items = state.search.items.filter((item) => item.id !== action.payload); + // Reset current if it's the same knowledge base + if (state.current.data && state.current.data.id === action.payload) { + state.current.data = null; + } + state.operations.error = null; + }) + .addCase(deleteKnowledgeBase.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload; + }); + }, +}); + +export const { resetOperationStatus, resetCurrentKnowledgeBase, resetSearchState } = knowledgeBaseSlice.actions; +const knowledgeBaseReducer = knowledgeBaseSlice.reducer; +export default knowledgeBaseReducer; diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js new file mode 100644 index 0000000..e961002 --- /dev/null +++ b/src/store/knowledgeBase/knowledgeBase.thunks.js @@ -0,0 +1,107 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put, del } from '../../services/api'; + +/** + * Fetch knowledge bases with pagination + * @param {Object} params - Pagination parameters + * @param {number} params.page - Page number (default: 1) + * @param {number} params.page_size - Page size (default: 10) + */ +export const fetchKnowledgeBases = createAsyncThunk( + 'knowledgeBase/fetchKnowledgeBases', + async ({ page = 1, page_size = 10 } = {}, { rejectWithValue }) => { + try { + const response = await get('/knowledge-bases/', { page, page_size }); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to fetch knowledge bases'); + } + } +); + +/** + * Search knowledge bases + * @param {Object} params - Search parameters + * @param {string} params.keyword - Search keyword + * @param {number} params.page - Page number (default: 1) + * @param {number} params.page_size - Page size (default: 10) + */ +export const searchKnowledgeBases = createAsyncThunk( + 'knowledgeBase/searchKnowledgeBases', + async ({ keyword, page = 1, page_size = 10 }, { rejectWithValue }) => { + try { + const response = await get('/knowledge-bases/search/', { + keyword, + page, + page_size, + }); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to search knowledge bases'); + } + } +); + +/** + * Create a new knowledge base + */ +export const createKnowledgeBase = createAsyncThunk( + 'knowledgeBase/createKnowledgeBase', + async (knowledgeBaseData, { rejectWithValue }) => { + try { + const response = await post('/knowledge-bases/', knowledgeBaseData); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to create knowledge base'); + } + } +); + +/** + * Get knowledge base details by ID + */ +export const getKnowledgeBaseById = createAsyncThunk( + 'knowledgeBase/getKnowledgeBaseById', + async (id, { rejectWithValue }) => { + try { + const response = await get(`/knowledge-bases/${id}/`); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to get knowledge base details'); + } + } +); + +/** + * Update knowledge base + * @param {Object} params - Update parameters + * @param {string} params.id - Knowledge base ID + * @param {Object} params.data - Update data (name, desc) + */ +export const updateKnowledgeBase = createAsyncThunk( + 'knowledgeBase/updateKnowledgeBase', + async ({ id, data }, { rejectWithValue }) => { + try { + const response = await put(`/knowledge-bases/${id}/`, data); + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to update knowledge base'); + } + } +); + +/** + * Delete knowledge base + * @param {string} id - Knowledge base ID + */ +export const deleteKnowledgeBase = createAsyncThunk( + 'knowledgeBase/deleteKnowledgeBase', + async (id, { rejectWithValue }) => { + try { + await del(`/knowledge-bases/${id}/`); + return id; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to delete knowledge base'); + } + } +); diff --git a/src/store/store.js b/src/store/store.js index 6e06a4d..f83d727 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -3,10 +3,12 @@ import { persistReducer, persistStore } from 'redux-persist'; import sessionStorage from 'redux-persist/lib/storage/session'; import notificationReducer from './notification.slice.js'; import authReducer from './auth/auth.slice.js'; +import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js'; const rootRducer = combineReducers({ auth: authReducer, notification: notificationReducer, + knowledgeBase: knowledgeBaseReducer, }); const persistConfig = { From 4915514bdef7b489cebe05c1c0e0c2e7d4482f69 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Fri, 7 Mar 2025 18:05:33 -0500 Subject: [PATCH 09/21] [dev]Update Knowledge base detail page Add mock data --- package-lock.json | 15 +- package.json | 3 +- src/pages/KnowledgeBase/Detail/DatasetTab.jsx | 32 +- .../{ => Detail}/KnowledgeBaseDetail.jsx | 12 +- .../KnowledgeBase/Detail/SettingsTab.jsx | 330 +------- .../Detail/components/Breadcrumb.jsx | 2 +- .../Detail/components/KnowledgeBaseForm.jsx | 26 +- .../components/UserPermissionsManager.jsx | 789 ++++++++++++++++++ src/pages/KnowledgeBase/KnowledgeBase.jsx | 67 +- .../components/CreateKnowledgeBaseModal.jsx | 4 +- .../components/KnowledgeBaseList.jsx | 11 +- .../{ => components}/KnowledgeCard.jsx | 30 +- src/router/router.jsx | 2 +- src/services/api.js | 46 + src/services/mockApi.js | 352 ++++++++ 15 files changed, 1356 insertions(+), 365 deletions(-) rename src/pages/KnowledgeBase/{ => Detail}/KnowledgeBaseDetail.jsx (92%) create mode 100644 src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx rename src/pages/KnowledgeBase/{ => components}/KnowledgeCard.jsx (80%) create mode 100644 src/services/mockApi.js diff --git a/package-lock.json b/package-lock.json index 12d3e6b..8e04914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ff431fc..ca40cf6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx index 9f9e027..ae4b1e8 100644 --- a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx +++ b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx @@ -270,7 +270,7 @@ export default function DatasetTab({ knowledgeBase }) {
    + +
  • + +
  • + + +
    +
    + {/* File upload modal */}
    {knowledgeBase.name}
    -

    {knowledgeBase.desc}

    +

    + {knowledgeBase.desc || knowledgeBase.description || ''} +


    diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx index 0b2705f..b257c3b 100644 --- a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -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 */} + + {/* Delete confirmation modal */} setShowDeleteConfirm(false)} onConfirm={handleDelete} /> - - {/* Edit User Permissions Modal */} - {showEditModal && ( -
    -
    -
    -
    编辑用户权限
    - -
    -
    -
    - -
    -
    -
    - ID: - #{editUser?.id} -
    -
    - 用户名: - {editUser?.username} -
    -
    - 邮箱: - {editUser?.email} -
    -
    -
    -
    - -
    - -
    -
    - handleEditInputChange({ target: { name: 'permissionType', value: '只读' } }) - } - > -
    只读
    -
    仅查看数据集内容
    -
    -
    - handleEditInputChange({ - target: { name: 'permissionType', value: '完全访问' }, - }) - } - > -
    完全访问
    -
    查看、编辑和管理数据
    -
    -
    - {formErrors.permissionType && ( -
    {formErrors.permissionType}
    - )} -
    - -
    - - - {formErrors.accessDuration && ( -
    {formErrors.accessDuration}
    - )} -
    -
    -
    - - -
    -
    -
    - )} ); } diff --git a/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx index 4d3b6ec..b3bc408 100644 --- a/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx +++ b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx @@ -10,7 +10,7 @@ const Breadcrumb = ({ knowledgeBase, activeTab }) => {
    - -
    - -

    {new Date(knowledgeBase.create_time).toLocaleString()}

    -
    - -
    - -

    {new Date(knowledgeBase.update_time).toLocaleString()}

    -
    -
    + {showBatchDropdown && ( +
      +
    • + +
    • +
    • + +
    • +
    + )} +
    + )} + +
    +
    + +
    + + + + + + + + + + + + + {currentUsers.length > 0 ? ( + currentUsers.map((user) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
    +
    + +
    +
    用户名邮箱权限类型访问期限操作
    +
    + handleSelectUser(user.id)} + /> +
    +
    {user.username}{user.email} + + {user.permissionType} + + {user.accessDuration} +
    + + +
    +
    + 暂无用户 +
    +
    + + {/* Pagination */} + {totalPages > 1 && ( + + )} + + {/* Edit User Modal */} + {showEditModal && ( +
    +
    +
    +
    +
    编辑用户权限
    + +
    +
    +
    +
    + + + {formErrors.username && ( +
    {formErrors.username}
    + )} +
    +
    + + + {formErrors.email && ( +
    {formErrors.email}
    + )} +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + )} + + {/* Add User Modal */} + {showAddUserModal && ( +
    +
    +
    +
    +
    添加用户
    + +
    +
    +
    +
    + + + {formErrors.username && ( +
    {formErrors.username}
    + )} +
    +
    + + + {formErrors.email && ( +
    {formErrors.email}
    + )} +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + )} + + {/* Batch Edit Modal */} + {showBatchEditModal && ( +
    +
    +
    +
    +
    批量修改权限
    + +
    +
    +

    + 您正在修改 {selectedUsers.length} 个用户的权限 +

    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + )} + + {/* Modal backdrop */} + {(showEditModal || showAddUserModal || showBatchEditModal) && ( +
    + )} +
    +
    + ); +}; + +export default UserPermissionsManager; diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index a0e65c0..af3fc32 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -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 ( -
    +
    {/* Pagination */} @@ -401,8 +409,47 @@ export default function KnowledgeBase() {
    + {/*
    + +
    +
    + setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' })) + } + > +
    只读访问
    +
    仅查看数据集内容
    +
    +
    + setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' })) + } + > +
    完全访问
    +
    查看、编辑和管理数据
    +
    +
    +
    */}
    - +
    +
    - +
    -
    +
    +
    + + +
    +
    +
    + ); +} diff --git a/src/components/CreateKnowledgeBaseModal.jsx b/src/components/CreateKnowledgeBaseModal.jsx new file mode 100644 index 0000000..4402387 --- /dev/null +++ b/src/components/CreateKnowledgeBaseModal.jsx @@ -0,0 +1,168 @@ +import React from 'react'; +import SvgIcon from './SvgIcon'; + +/** + * 创建知识库模态框组件 + * @param {Object} props + * @param {boolean} props.show - 是否显示弹窗 + * @param {Object} props.formData - 表单数据 + * @param {Object} props.formErrors - 表单错误信息 + * @param {boolean} props.isSubmitting - 是否正在提交 + * @param {Function} props.onClose - 关闭弹窗的回调函数 + * @param {Function} props.onChange - 表单输入变化的回调函数 + * @param {Function} props.onSubmit - 提交表单的回调函数 + */ +const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => { + if (!show) return null; + + return ( +
    +
    +
    +
    新建知识库
    + +
    +
    +
    + + + {formErrors.name &&
    {formErrors.name}
    } +
    +
    + + + {formErrors.desc &&
    {formErrors.desc}
    } +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + {formErrors.type &&
    {formErrors.type}
    } +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    + ); +}; + +export default CreateKnowledgeBaseModal; diff --git a/src/pages/KnowledgeBase/components/Pagination.jsx b/src/components/Pagination.jsx similarity index 84% rename from src/pages/KnowledgeBase/components/Pagination.jsx rename to src/components/Pagination.jsx index 8c8f4b1..c819887 100644 --- a/src/pages/KnowledgeBase/components/Pagination.jsx +++ b/src/components/Pagination.jsx @@ -2,6 +2,12 @@ import React from 'react'; /** * 分页组件 + * @param {Object} props + * @param {number} props.currentPage - 当前页码 + * @param {number} props.totalPages - 总页数 + * @param {number} props.pageSize - 每页显示的条目数 + * @param {Function} props.onPageChange - 页码变化的回调函数 + * @param {Function} props.onPageSizeChange - 每页条目数变化的回调函数 */ const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => { return ( @@ -17,7 +23,7 @@ const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSiz
    -
    {/* Main Content */} diff --git a/src/pages/Chat/ChatSidebar.jsx b/src/pages/Chat/ChatSidebar.jsx index 133bdef..9eaf52b 100644 --- a/src/pages/Chat/ChatSidebar.jsx +++ b/src/pages/Chat/ChatSidebar.jsx @@ -2,9 +2,9 @@ import React, { useState } from 'react'; import { Link, useNavigate, useParams } from 'react-router-dom'; import SvgIcon from '../../components/SvgIcon'; -export default function ChatSidebar({ chatHistory, onDeleteChat }) { +export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading = false, hasError = false }) { const navigate = useNavigate(); - const { chatId } = useParams(); + const { chatId, knowledgeBaseId } = useParams(); const [activeDropdown, setActiveDropdown] = useState(null); const handleNewChat = () => { @@ -28,10 +28,37 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) { setActiveDropdown(null); }; + // 渲染加载状态 + const renderLoading = () => ( +
    +
    + 加载中... +
    +
    加载聊天记录...
    +
    + ); + + // 渲染错误状态 + const renderError = () => ( +
    +
    + +
    +
    加载聊天记录失败,请重试
    +
    + ); + + // 渲染空状态 + const renderEmpty = () => ( +
    +
    暂无聊天记录
    +
    + ); + return (
    -
    Chats
    +
    聊天记录
    @@ -40,54 +67,62 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) { onClick={handleNewChat} > - New Chat + 新建聊天
    -
      - {chatHistory.map((chat) => ( -
    • - + {chatHistory.map((chat) => ( +
    • -
      {chat.title}
      - -
      handleMouseEnter(chat.id)} - onMouseLeave={handleMouseLeave} - > - - {activeDropdown === chat.id && ( -
      - + {activeDropdown === chat.id && ( +
      - - 删除 - -
      - )} -
      -
    • - ))} -
    + +
    + )} +
    + + ))} + + )} ); diff --git a/src/pages/Chat/ChatWindow.jsx b/src/pages/Chat/ChatWindow.jsx index 1fa74fe..b8cf4c4 100644 --- a/src/pages/Chat/ChatWindow.jsx +++ b/src/pages/Chat/ChatWindow.jsx @@ -1,62 +1,52 @@ import React, { useState, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchMessages, sendMessage } from '../../store/chat/chat.messages.thunks'; +import { resetMessages, resetSendMessageStatus } from '../../store/chat/chat.slice'; +import { showNotification } from '../../store/notification.slice'; import SvgIcon from '../../components/SvgIcon'; export default function ChatWindow({ chatId, knowledgeBaseId }) { - const [messages, setMessages] = useState([]); + const dispatch = useDispatch(); const [inputMessage, setInputMessage] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [knowledgeBase, setKnowledgeBase] = useState(null); const messagesEndRef = useRef(null); - // Fetch knowledge base details + // 从 Redux store 获取数据 + const { + items: messages, + status: messagesStatus, + error: messagesError, + } = useSelector((state) => state.chat.messages); + const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage); + const knowledgeBase = useSelector((state) => + state.knowledgeBase.list.items.find((kb) => kb.id === knowledgeBaseId) + ); + + // 获取聊天消息 useEffect(() => { - // In a real app, you would fetch the knowledge base details from the API - const mockKnowledgeBases = [ - { - id: '1', - title: '产品开发知识库', - description: '产品开发流程及规范说明文档', - }, - { - id: '2', - title: '市场分析知识库', - description: '2025年Q1市场分析总结', - }, - { - id: '3', - title: '财务知识库', - description: '月度财务分析报告', - }, - { - id: '4', - title: '技术架构知识库', - description: '系统架构设计文档', - }, - { - id: '5', - title: '用户研究知识库', - description: '用户调研和反馈分析', - }, - ]; - - const kb = mockKnowledgeBases.find((kb) => kb.id === knowledgeBaseId); - setKnowledgeBase(kb); - - // In a real app, you would fetch the chat messages from the API - // For now, we'll just add a welcome message - if (kb) { - setMessages([ - { - id: '1', - sender: 'bot', - content: `欢迎使用 ${kb.title},有什么可以帮助您的?`, - timestamp: new Date().toISOString(), - }, - ]); + if (chatId) { + dispatch(fetchMessages(chatId)); } - }, [chatId, knowledgeBaseId]); - // Scroll to bottom when messages change + // 组件卸载时重置消息状态 + return () => { + dispatch(resetMessages()); + }; + }, [chatId, dispatch]); + + // 监听发送消息状态 + useEffect(() => { + if (sendStatus === 'failed' && sendError) { + dispatch( + showNotification({ + message: `发送失败: ${sendError}`, + type: 'danger', + }) + ); + dispatch(resetSendMessageStatus()); + } + }, [sendStatus, sendError, dispatch]); + + // 滚动到底部 useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); @@ -64,68 +54,83 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { const handleSendMessage = (e) => { e.preventDefault(); - if (!inputMessage.trim()) return; + if (!inputMessage.trim() || sendStatus === 'loading') return; - // Add user message - const userMessage = { - id: Date.now().toString(), - sender: 'user', - content: inputMessage, - timestamp: new Date().toISOString(), - }; - - setMessages((prev) => [...prev, userMessage]); + // 发送消息 + dispatch(sendMessage({ chatId, content: inputMessage })); setInputMessage(''); - setIsLoading(true); - - // Simulate bot response after a delay - setTimeout(() => { - const botMessage = { - id: (Date.now() + 1).toString(), - sender: 'bot', - content: `这是来自 ${knowledgeBase?.title} 的回复:${inputMessage}`, - timestamp: new Date().toISOString(), - }; - - setMessages((prev) => [...prev, botMessage]); - setIsLoading(false); - }, 1000); }; + // 渲染加载状态 + const renderLoading = () => ( +
    +
    + 加载中... +
    +
    加载聊天记录...
    +
    + ); + + // 渲染错误状态 + const renderError = () => ( +
    +
    + +
    +
    加载聊天记录失败,请重试
    + +
    + ); + + // 渲染空状态 + const renderEmpty = () => ( +
    +
    暂无聊天记录,发送一条消息开始聊天吧
    +
    + ); + return (
    {/* Chat header */}
    -
    {knowledgeBase?.title || 'Loading...'}
    +
    {knowledgeBase?.name || '加载中...'}
    {knowledgeBase?.description}
    {/* Chat messages */} -
    +
    - {messages.map((message) => ( -
    -
    - {message.content} -
    -
    - ))} + {messagesStatus === 'loading' + ? renderLoading() + : messagesStatus === 'failed' + ? renderError() + : messages.length === 0 + ? renderEmpty() + : messages.map((message) => ( +
    +
    + {message.content} +
    +
    + ))} - {isLoading && ( + {sendStatus === 'loading' && (
    - Loading... + 加载中...
    @@ -144,15 +149,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { placeholder='输入你的问题...' value={inputMessage} onChange={(e) => setInputMessage(e.target.value)} - disabled={isLoading} + disabled={sendStatus === 'loading'} />
    diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx index 1d54400..873fe50 100644 --- a/src/pages/Chat/NewChat.jsx +++ b/src/pages/Chat/NewChat.jsx @@ -1,54 +1,47 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { showNotification } from '../../store/notification.slice'; +import { get } from '../../services/api'; import SvgIcon from '../../components/SvgIcon'; export default function NewChat() { const navigate = useNavigate(); - const [knowledgeBases, setKnowledgeBases] = useState([ - { - id: '1', - title: '产品开发知识库', - description: '产品开发流程及规范说明文档', - documents: 24, - date: '2025-02-15', - access: 'full', - }, - { - id: '2', - title: '市场分析知识库', - description: '2025年Q1市场分析总结', - documents: 12, - date: '2025-02-10', - access: 'read', - }, - { - id: '3', - title: '财务知识库', - description: '月度财务分析报告', - documents: 8, - date: '2025-02-01', - access: 'none', - }, - { - id: '4', - title: '技术架构知识库', - description: '系统架构设计文档', - documents: 15, - date: '2025-01-20', - access: 'full', - }, - { - id: '5', - title: '用户研究知识库', - description: '用户调研和反馈分析', - documents: 18, - date: '2025-01-15', - access: 'read', - }, - ]); + const dispatch = useDispatch(); + const [knowledgeBases, setKnowledgeBases] = useState([]); + const [loading, setLoading] = useState(true); + + // 获取知识库列表 + useEffect(() => { + const fetchKnowledgeBases = async () => { + try { + setLoading(true); + const response = await get('/knowledge-bases/'); + + // 过滤出有 can_read 权限的知识库 + const readableKnowledgeBases = response.data.items.filter( + (kb) => kb.permissions && kb.permissions.can_read === true + ); + + setKnowledgeBases(readableKnowledgeBases); + } catch (error) { + console.error('获取知识库列表失败:', error); + dispatch( + showNotification({ + message: '获取知识库列表失败,请稍后重试', + type: 'danger', + }) + ); + } finally { + setLoading(false); + } + }; + + fetchKnowledgeBases(); + }, [dispatch]); const handleSelectKnowledgeBase = (knowledgeBaseId) => { - // In a real app, you would create a new chat and get its ID from the API + // 创建新聊天并导航到聊天页面 navigate(`/chat/${knowledgeBaseId}`); }; @@ -58,9 +51,19 @@ export default function NewChat() {

    选择知识库开始聊天

    -
    - {knowledgeBases.map((kb) => - kb.access === 'full' || kb.access === 'read' ? ( + {loading ? ( +
    +
    + 加载中... +
    +
    + ) : knowledgeBases.length === 0 ? ( +
    +

    没有可用的知识库,请联系管理员获取权限

    +
    + ) : ( +
    + {knowledgeBases.map((kb) => (
    -

    {kb.title}

    +

    {kb.name}

    {kb.description}

    +
    + 文档数: {kb.document_count || 0} +
    - ) : null - )} -
    + ))} +
    + )}
    ); } diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx index b257c3b..a38eab8 100644 --- a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -19,6 +19,8 @@ export default function SettingsTab({ knowledgeBase }) { name: knowledgeBase.name, desc: knowledgeBase.desc || knowledgeBase.description || '', type: knowledgeBase.type || 'private', // 默认为私有知识库 + department: knowledgeBase.department || '', + group: knowledgeBase.group || '', }); const [formErrors, setFormErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); @@ -81,6 +83,8 @@ export default function SettingsTab({ knowledgeBase }) { desc: knowledgeBaseForm.desc, description: knowledgeBaseForm.desc, // Add description field for compatibility type: knowledgeBaseForm.type, + department: knowledgeBaseForm.department, + group: knowledgeBaseForm.group, }, }) ) diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx index 0ccbb0e..2256a7a 100644 --- a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx +++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx @@ -86,13 +86,13 @@ const FileUploadModal = ({ >
    -
    +
    + +
    + + + 部门信息根据知识库创建者自动填写 +
    + +
    + + + 组别信息根据知识库创建者自动填写 +
    +
    @@ -371,166 +379,14 @@ export default function KnowledgeBase() { /> {/* 申请权限弹窗 */} - {showAccessRequestModal && ( -
    -
    -
    -
    申请访问权限
    - -
    -
    -
    - - -
    - {/*
    - -
    -
    - setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' })) - } - > -
    只读访问
    -
    仅查看数据集内容
    -
    -
    - setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' })) - } - > -
    完全访问
    -
    查看、编辑和管理数据
    -
    -
    -
    */} -
    - - -
    - -
    - - -
    -
    - - - {accessRequestErrors.projectInfo && ( -
    {accessRequestErrors.projectInfo}
    - )} -
    -
    - - - {accessRequestErrors.reason && ( -
    {accessRequestErrors.reason}
    - )} -
    -
    -
    - - -
    -
    -
    - )} + setShowAccessRequestModal(false)} + onSubmit={handleSubmitAccessRequest} + isSubmitting={isSubmittingRequest} + />
    ); } diff --git a/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx b/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx deleted file mode 100644 index 8c8b4c4..0000000 --- a/src/pages/KnowledgeBase/components/CreateKnowledgeBaseModal.jsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import SvgIcon from '../../../components/SvgIcon'; - -/** - * 创建知识库模态框组件 - */ -const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => { - if (!show) return null; - - return ( -
    -
    -
    -
    新建知识库
    - -
    -
    -
    - - - {formErrors.name &&
    {formErrors.name}
    } -
    -
    - - - {formErrors.desc &&
    {formErrors.desc}
    } -
    -
    -
    - - -
    -
    -
    - ); -}; - -export default CreateKnowledgeBaseModal; diff --git a/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx index 29fb2cc..9aea72e 100644 --- a/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx +++ b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx @@ -23,8 +23,9 @@ const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequest description={item.description || item.desc || ''} documents={item.document_count || 0} date={new Date(item.create_time || item.created_at).toLocaleDateString()} + permissions={item.permissions} access={item.permissions?.can_edit ? 'full' : item.permissions?.can_read ? 'read' : 'none'} - onClick={() => onCardClick(item.id)} + onClick={() => onCardClick(item.id, item.permissions)} onRequestAccess={onRequestAccess} onDelete={(e) => onDelete(e, item.id)} /> diff --git a/src/pages/KnowledgeBase/components/KnowledgeCard.jsx b/src/pages/KnowledgeBase/components/KnowledgeCard.jsx index 479563b..0930673 100644 --- a/src/pages/KnowledgeBase/components/KnowledgeCard.jsx +++ b/src/pages/KnowledgeBase/components/KnowledgeCard.jsx @@ -9,6 +9,7 @@ export default function KnowledgeCard({ documents, date, access, + permissions, onClick, onRequestAccess, onDelete, @@ -41,17 +42,19 @@ export default function KnowledgeCard({
    {title}
    -
    - -
      -
    • - 删除 - -
    • -
    -
    + {permissions && permissions.can_delete && ( +
    + +
      +
    • + 删除 + +
    • +
    +
    + )}

    {description}

    diff --git a/src/pages/KnowledgeBase/components/SearchBar.jsx b/src/pages/KnowledgeBase/components/SearchBar.jsx deleted file mode 100644 index d20a2e6..0000000 --- a/src/pages/KnowledgeBase/components/SearchBar.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import SvgIcon from '../../../components/SvgIcon'; - -/** - * 知识库搜索栏组件 - */ -const SearchBar = ({ searchKeyword, isSearching, onSearchChange, onSearch, onClearSearch }) => { - return ( -
    - - {/* */} - {isSearching && ( - - )} -
    - ); -}; - -export default SearchBar; diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.jsx index 0183a9d..b325660 100644 --- a/src/pages/auth/Login.jsx +++ b/src/pages/auth/Login.jsx @@ -6,8 +6,8 @@ import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk'; export default function Login() { const dispatch = useDispatch(); const navigate = useNavigate(); - const [username, setUsername] = useState('member2'); - const [password, setPassword] = useState('member123'); + const [username, setUsername] = useState('leader2'); + const [password, setPassword] = useState('leader123'); const [errors, setErrors] = useState({}); const [submitted, setSubmitted] = useState(false); diff --git a/src/services/api.js b/src/services/api.js index 894c2ba..3047036 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,12 +1,8 @@ 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', @@ -62,30 +58,12 @@ 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 @@ -96,15 +74,6 @@ 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' }, }); @@ -113,25 +82,11 @@ 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: { diff --git a/src/services/mockApi.js b/src/services/mockApi.js index ddbacd8..38f358f 100644 --- a/src/services/mockApi.js +++ b/src/services/mockApi.js @@ -12,10 +12,14 @@ const mockKnowledgeBases = [ create_time: '2023-10-15T08:30:00Z', update_time: '2023-12-20T14:45:00Z', type: 'private', + department: '研发部', + group: '前端开发组', owner: { id: 'user-001', username: 'johndoe', email: 'john@example.com', + department: '研发部', + group: '前端开发组', }, document_count: 15, tags: ['react', 'javascript', 'frontend'], @@ -216,22 +220,89 @@ const paginate = (array, page_size, page) => { }; }; +// 导入聊天历史模拟数据和方法 +import { + mockChatHistory, + mockGetChatHistory, + mockCreateChat, + mockUpdateChat, + mockDeleteChat, +} from '../store/chatHistory/chatHistory.mock'; + +// 模拟聊天消息数据 +const chatMessages = {}; + // Mock API functions -export const mockGet = async (url, params = {}) => { - console.log(`[MOCK API] GET ${url}`, params); +export const mockGet = async (url, config = {}) => { + console.log(`[MOCK API] GET ${url}`, config); // Simulate network delay await new Promise((resolve) => setTimeout(resolve, 500)); - // Knowledge bases list with pagination + // Get knowledge bases if (url === '/knowledge-bases/') { - const { page = 1, page_size = 10 } = params; - return paginate(knowledgeBases, page_size, page); + return { + data: { + items: knowledgeBases, + total: knowledgeBases.length, + }, + }; + } + + // Get 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 { data: knowledgeBase }; + } + + // Get chat history + if (url === '/chat-history/') { + const params = config.params || { page: 1, page_size: 10 }; + return { data: mockGetChatHistory(params) }; + } + + // Get chat messages + if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) { + const chatId = url.split('/')[2]; + + // 如果没有该聊天的消息记录,创建一个空数组 + if (!chatMessages[chatId]) { + chatMessages[chatId] = []; + + // 添加一条欢迎消息 + const chat = mockChatHistory.find((chat) => chat.id === chatId); + if (chat) { + chatMessages[chatId].push({ + id: uuidv4(), + chat_id: chatId, + sender: 'bot', + content: `欢迎使用 ${chat.knowledge_base_name},有什么可以帮助您的?`, + type: 'text', + created_at: new Date().toISOString(), + }); + } + } + + return { + data: { + code: 200, + message: '获取成功', + data: { + messages: chatMessages[chatId] || [], + }, + }, + }; } // Knowledge base search if (url === '/knowledge-bases/search/') { - const { keyword = '', page = 1, page_size = 10 } = params; + const { keyword = '', page = 1, page_size = 10 } = config.params || {}; const filtered = knowledgeBases.filter( (kb) => kb.name.toLowerCase().includes(keyword.toLowerCase()) || @@ -241,18 +312,6 @@ export const mockGet = async (url, params = {}) => { 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' } } }; }; @@ -260,7 +319,7 @@ export const mockPost = async (url, data) => { console.log(`[MOCK API] POST ${url}`, data); // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 700)); + await new Promise((resolve) => setTimeout(resolve, 800)); // Create knowledge base if (url === '/knowledge-bases/') { @@ -274,6 +333,8 @@ export const mockPost = async (url, data) => { create_time: new Date().toISOString(), update_time: new Date().toISOString(), type: data.type || 'private', + department: data.department || null, + group: data.group || null, owner: { id: 'user-001', username: 'johndoe', @@ -289,7 +350,75 @@ export const mockPost = async (url, data) => { }; knowledgeBases.push(newKnowledgeBase); - return newKnowledgeBase; + + // 模拟后端返回格式 + return { + code: 200, + message: '知识库创建成功', + data: { + knowledge_base: newKnowledgeBase, + external_id: uuidv4(), + }, + }; + } + + // Create new chat + if (url === '/chat-history/') { + return { data: mockCreateChat(data) }; + } + + // Send chat message + if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) { + const chatId = url.split('/')[2]; + + // 如果没有该聊天的消息记录,创建一个空数组 + if (!chatMessages[chatId]) { + chatMessages[chatId] = []; + } + + // 创建用户消息 + const userMessage = { + id: uuidv4(), + chat_id: chatId, + sender: 'user', + content: data.content, + type: data.type || 'text', + created_at: new Date().toISOString(), + }; + + // 添加用户消息 + chatMessages[chatId].push(userMessage); + + // 创建机器人回复 + const botMessage = { + id: uuidv4(), + chat_id: chatId, + sender: 'bot', + content: `这是对您问题的回复:${data.content}`, + type: 'text', + created_at: new Date(Date.now() + 1000).toISOString(), + }; + + // 添加机器人回复 + chatMessages[chatId].push(botMessage); + + // 更新聊天的最后一条消息和时间 + const chatIndex = mockChatHistory.findIndex((chat) => chat.id === chatId); + if (chatIndex !== -1) { + mockChatHistory[chatIndex].message_count = (mockChatHistory[chatIndex].message_count || 0) + 2; + mockChatHistory[chatIndex].updated_at = new Date().toISOString(); + } + + return { + data: { + code: 200, + message: '发送成功', + data: { + user_message: userMessage, + bot_message: botMessage, + }, + }, + }; } throw { response: { status: 404, data: { message: 'Not found' } } }; @@ -318,7 +447,22 @@ export const mockPut = async (url, data) => { }; knowledgeBases[index] = updatedKnowledgeBase; - return updatedKnowledgeBase; + + // 返回与 mockPost 类似的格式 + return { + code: 200, + message: '知识库更新成功', + data: { + knowledge_base: updatedKnowledgeBase, + external_id: knowledgeBases[index].id, + }, + }; + } + + // Update chat + if (url.match(/^\/chat-history\/[^/]+\/$/)) { + const id = url.split('/')[2]; + return { data: mockUpdateChat(id, data) }; } throw { response: { status: 404, data: { message: 'Not found' } } }; @@ -343,6 +487,12 @@ export const mockDel = async (url) => { return { success: true }; } + // Delete chat + if (url.match(/^\/chat-history\/[^/]+\/$/)) { + const id = url.split('/')[2]; + return { data: mockDeleteChat(id) }; + } + throw { response: { status: 404, data: { message: 'Not found' } } }; }; diff --git a/src/store/auth/auth.mock.js b/src/store/auth/auth.mock.js new file mode 100644 index 0000000..72c6d6d --- /dev/null +++ b/src/store/auth/auth.mock.js @@ -0,0 +1,13 @@ +// 模拟的当前用户数据 +export const mockCurrentUser = { + id: 'user-001', + username: 'johndoe', + email: 'john@example.com', + name: 'John Doe', + department: '研发部', + group: '前端开发组', + role: 'developer', + avatar: 'https://via.placeholder.com/150', + created_at: '2023-01-15T08:30:00Z', + updated_at: '2023-12-20T14:45:00Z', +}; diff --git a/src/store/auth/auth.slice.js b/src/store/auth/auth.slice.js index 615a97a..ffde388 100644 --- a/src/store/auth/auth.slice.js +++ b/src/store/auth/auth.slice.js @@ -1,5 +1,6 @@ import { createSlice } from '@reduxjs/toolkit'; import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk'; +import { mockCurrentUser } from './auth.mock'; const setPending = (state) => { state.loading = true; @@ -20,7 +21,11 @@ const setRejected = (state, action) => { const authSlice = createSlice({ name: 'auth', - initialState: { loading: false, error: null, user: null }, + initialState: { + loading: false, + error: null, + user: mockCurrentUser, // 使用模拟的当前用户数据 + }, reducers: { login: (state, action) => { state.user = action.payload; diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js index 8efd5bb..4ce2924 100644 --- a/src/store/auth/auth.thunk.js +++ b/src/store/auth/auth.thunk.js @@ -54,7 +54,7 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => { try { - const { user, message } = await get('/auth/verify-token/'); + const { user, message } = await post('/auth/verify-token/'); if (!user) { dispatch(logout()); throw new Error(message || 'No token found'); diff --git a/src/store/chat/chat.messages.thunks.js b/src/store/chat/chat.messages.thunks.js new file mode 100644 index 0000000..e59e461 --- /dev/null +++ b/src/store/chat/chat.messages.thunks.js @@ -0,0 +1,45 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post } from '../../services/api'; + +/** + * 获取聊天消息 + * @param {string} chatId - 聊天ID + */ +export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (chatId, { rejectWithValue }) => { + try { + const response = await get(`/chat-history/${chatId}/messages/`); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data.messages; + } + + return response.data?.messages || []; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to fetch messages'); + } +}); + +/** + * 发送聊天消息 + * @param {Object} params - 消息参数 + * @param {string} params.chatId - 聊天ID + * @param {string} params.content - 消息内容 + */ +export const sendMessage = createAsyncThunk('chat/sendMessage', async ({ chatId, content }, { rejectWithValue }) => { + try { + const response = await post(`/chat-history/${chatId}/messages/`, { + content, + type: 'text', + }); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data; + } + + return response.data || {}; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to send message'); + } +}); diff --git a/src/store/chat/chat.slice.js b/src/store/chat/chat.slice.js new file mode 100644 index 0000000..f1644d2 --- /dev/null +++ b/src/store/chat/chat.slice.js @@ -0,0 +1,190 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { fetchChats, createChat, deleteChat, updateChat } from './chat.thunks'; +import { fetchMessages, sendMessage } from './chat.messages.thunks'; + +// 初始状态 +const initialState = { + // 聊天列表 + list: { + items: [], + total: 0, + page: 1, + page_size: 10, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + + // 当前聊天 + currentChat: { + data: null, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + + // 聊天消息 + messages: { + items: [], + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + + // 发送消息状态 + sendMessage: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + + // 操作状态(创建、更新、删除) + operations: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, +}; + +// 创建 slice +const chatSlice = createSlice({ + name: 'chat', + initialState, + reducers: { + // 重置操作状态 + resetOperationStatus: (state) => { + state.operations.status = 'idle'; + state.operations.error = null; + }, + + // 重置当前聊天 + resetCurrentChat: (state) => { + state.currentChat.data = null; + state.currentChat.status = 'idle'; + state.currentChat.error = null; + }, + + // 设置当前聊天 + setCurrentChat: (state, action) => { + state.currentChat.data = action.payload; + state.currentChat.status = 'succeeded'; + }, + + // 重置消息状态 + resetMessages: (state) => { + state.messages.items = []; + state.messages.status = 'idle'; + state.messages.error = null; + }, + + // 重置发送消息状态 + resetSendMessageStatus: (state) => { + state.sendMessage.status = 'idle'; + state.sendMessage.error = null; + }, + }, + extraReducers: (builder) => { + // 获取聊天列表 + builder + .addCase(fetchChats.pending, (state) => { + state.list.status = 'loading'; + }) + .addCase(fetchChats.fulfilled, (state, action) => { + state.list.status = 'succeeded'; + state.list.items = action.payload.results; + state.list.total = action.payload.total; + state.list.page = action.payload.page; + state.list.page_size = action.payload.page_size; + }) + .addCase(fetchChats.rejected, (state, action) => { + state.list.status = 'failed'; + state.list.error = action.payload || action.error.message; + }) + + // 创建聊天 + .addCase(createChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(createChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + state.list.items.unshift(action.payload); + state.list.total += 1; + state.currentChat.data = action.payload; + state.currentChat.status = 'succeeded'; + }) + .addCase(createChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 删除聊天 + .addCase(deleteChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(deleteChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + state.list.items = state.list.items.filter((chat) => chat.id !== action.payload); + state.list.total -= 1; + if (state.currentChat.data && state.currentChat.data.id === action.payload) { + state.currentChat.data = null; + } + }) + .addCase(deleteChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 更新聊天 + .addCase(updateChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(updateChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + const index = state.list.items.findIndex((chat) => chat.id === action.payload.id); + if (index !== -1) { + state.list.items[index] = action.payload; + } + if (state.currentChat.data && state.currentChat.data.id === action.payload.id) { + state.currentChat.data = action.payload; + } + }) + .addCase(updateChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 获取聊天消息 + .addCase(fetchMessages.pending, (state) => { + state.messages.status = 'loading'; + }) + .addCase(fetchMessages.fulfilled, (state, action) => { + state.messages.status = 'succeeded'; + state.messages.items = action.payload; + }) + .addCase(fetchMessages.rejected, (state, action) => { + state.messages.status = 'failed'; + state.messages.error = action.payload || action.error.message; + }) + + // 发送聊天消息 + .addCase(sendMessage.pending, (state) => { + state.sendMessage.status = 'loading'; + }) + .addCase(sendMessage.fulfilled, (state, action) => { + state.sendMessage.status = 'succeeded'; + // 添加用户消息和机器人回复 + if (action.payload.user_message) { + state.messages.items.push(action.payload.user_message); + } + if (action.payload.bot_message) { + state.messages.items.push(action.payload.bot_message); + } + }) + .addCase(sendMessage.rejected, (state, action) => { + state.sendMessage.status = 'failed'; + state.sendMessage.error = action.payload || action.error.message; + }); + }, +}); + +// 导出 actions +export const { resetOperationStatus, resetCurrentChat, setCurrentChat, resetMessages, resetSendMessageStatus } = + chatSlice.actions; + +// 导出 reducer +export default chatSlice.reducer; diff --git a/src/store/chat/chat.thunks.js b/src/store/chat/chat.thunks.js new file mode 100644 index 0000000..9bd22e9 --- /dev/null +++ b/src/store/chat/chat.thunks.js @@ -0,0 +1,87 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put, del } from '../../services/api'; + +/** + * 获取聊天列表 + * @param {Object} params - 查询参数 + * @param {number} params.page - 页码 + * @param {number} params.page_size - 每页数量 + */ +export const fetchChats = createAsyncThunk( + 'chat/fetchChats', + async (params = { page: 1, page_size: 10 }, { rejectWithValue }) => { + try { + const response = await get('/chat-history/', { params }); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data; + } + + return response.data || { results: [], total: 0, page: 1, page_size: 10 }; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to fetch chats'); + } + } +); + +/** + * 创建新聊天 + * @param {Object} chatData - 聊天数据 + * @param {string} chatData.knowledge_base_id - 知识库ID + * @param {string} chatData.title - 聊天标题 + */ +export const createChat = createAsyncThunk('chat/createChat', async (chatData, { rejectWithValue }) => { + try { + const response = await post('/chat-history/', chatData); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data.chat; + } + + return response.data?.chat || {}; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to create chat'); + } +}); + +/** + * 更新聊天 + * @param {Object} params - 更新参数 + * @param {string} params.id - 聊天ID + * @param {Object} params.data - 更新数据 + */ +export const updateChat = createAsyncThunk('chat/updateChat', async ({ id, data }, { rejectWithValue }) => { + try { + const response = await put(`/chat-history/${id}/`, data); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data.chat; + } + + return response.data?.chat || {}; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to update chat'); + } +}); + +/** + * 删除聊天 + * @param {string} id - 聊天ID + */ +export const deleteChat = createAsyncThunk('chat/deleteChat', async (id, { rejectWithValue }) => { + try { + const response = await del(`/chat-history/${id}/`); + + // 处理返回格式 + if (response && response.code === 200) { + return id; + } + + return id; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to delete chat'); + } +}); diff --git a/src/store/chatHistory/chatHistory.mock.js b/src/store/chatHistory/chatHistory.mock.js new file mode 100644 index 0000000..088d3c7 --- /dev/null +++ b/src/store/chatHistory/chatHistory.mock.js @@ -0,0 +1,153 @@ +import { v4 as uuidv4 } from 'uuid'; + +// 模拟聊天历史数据 +export const mockChatHistory = [ + { + id: '1', + title: '关于产品开发流程的咨询', + knowledge_base_id: '1', + knowledge_base_name: '产品开发知识库', + created_at: '2025-03-10T10:30:00Z', + updated_at: '2025-03-10T11:45:00Z', + message_count: 8, + }, + { + id: '2', + title: '市场分析报告查询', + knowledge_base_id: '2', + knowledge_base_name: '市场分析知识库', + created_at: '2025-03-09T14:20:00Z', + updated_at: '2025-03-09T15:10:00Z', + message_count: 5, + }, + { + id: '3', + title: '技术架构设计讨论', + knowledge_base_id: '4', + knowledge_base_name: '技术架构知识库', + created_at: '2025-03-08T09:15:00Z', + updated_at: '2025-03-08T10:30:00Z', + message_count: 12, + }, + { + id: '4', + title: '用户反馈分析', + knowledge_base_id: '5', + knowledge_base_name: '用户研究知识库', + created_at: '2025-03-07T16:40:00Z', + updated_at: '2025-03-07T17:25:00Z', + message_count: 6, + }, +]; + +// 内存存储,用于模拟数据库操作 +let chatHistoryStore = [...mockChatHistory]; + +/** + * 模拟获取聊天历史列表 + * @param {Object} params - 查询参数 + * @returns {Object} - 分页结果 + */ +export const mockGetChatHistory = (params = { page: 1, page_size: 10 }) => { + const { page, page_size } = params; + const startIndex = (page - 1) * page_size; + const endIndex = startIndex + page_size; + const paginatedItems = chatHistoryStore.slice(startIndex, endIndex); + + return { + code: 200, + message: '获取成功', + data: { + total: chatHistoryStore.length, + page, + page_size, + results: paginatedItems, + }, + }; +}; + +/** + * 模拟创建新聊天 + * @param {Object} chatData - 聊天数据 + * @returns {Object} - 创建结果 + */ +export const mockCreateChat = (chatData) => { + const newChat = { + id: uuidv4(), + title: chatData.title || '新的聊天', + knowledge_base_id: chatData.knowledge_base_id, + knowledge_base_name: chatData.knowledge_base_name || '未知知识库', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + message_count: 0, + }; + + chatHistoryStore.unshift(newChat); + + return { + code: 200, + message: '创建成功', + data: { + chat: newChat, + }, + }; +}; + +/** + * 模拟更新聊天 + * @param {string} id - 聊天ID + * @param {Object} data - 更新数据 + * @returns {Object} - 更新结果 + */ +export const mockUpdateChat = (id, data) => { + const index = chatHistoryStore.findIndex((chat) => chat.id === id); + + if (index === -1) { + return { + code: 404, + message: '聊天不存在', + data: null, + }; + } + + const updatedChat = { + ...chatHistoryStore[index], + ...data, + updated_at: new Date().toISOString(), + }; + + chatHistoryStore[index] = updatedChat; + + return { + code: 200, + message: '更新成功', + data: { + chat: updatedChat, + }, + }; +}; + +/** + * 模拟删除聊天 + * @param {string} id - 聊天ID + * @returns {Object} - 删除结果 + */ +export const mockDeleteChat = (id) => { + const index = chatHistoryStore.findIndex((chat) => chat.id === id); + + if (index === -1) { + return { + code: 404, + message: '聊天不存在', + data: null, + }; + } + + chatHistoryStore.splice(index, 1); + + return { + code: 200, + message: '删除成功', + data: null, + }; +}; diff --git a/src/store/chatHistory/chatHistory.slice.js b/src/store/chatHistory/chatHistory.slice.js new file mode 100644 index 0000000..3a9743a --- /dev/null +++ b/src/store/chatHistory/chatHistory.slice.js @@ -0,0 +1,130 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { fetchChatHistory, createChat, deleteChat, updateChat } from './chatHistory.thunks'; + +// 初始状态 +const initialState = { + // 聊天历史列表 + list: { + items: [], + total: 0, + page: 1, + page_size: 10, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + + // 当前聊天 + currentChat: { + data: null, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + + // 操作状态(创建、更新、删除) + operations: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, +}; + +// 创建 slice +const chatHistorySlice = createSlice({ + name: 'chatHistory', + initialState, + reducers: { + // 重置操作状态 + resetOperationStatus: (state) => { + state.operations.status = 'idle'; + state.operations.error = null; + }, + + // 重置当前聊天 + resetCurrentChat: (state) => { + state.currentChat.data = null; + state.currentChat.status = 'idle'; + state.currentChat.error = null; + }, + + // 设置当前聊天 + setCurrentChat: (state, action) => { + state.currentChat.data = action.payload; + state.currentChat.status = 'succeeded'; + }, + }, + extraReducers: (builder) => { + // 获取聊天历史 + builder + .addCase(fetchChatHistory.pending, (state) => { + state.list.status = 'loading'; + }) + .addCase(fetchChatHistory.fulfilled, (state, action) => { + state.list.status = 'succeeded'; + state.list.items = action.payload.results; + state.list.total = action.payload.total; + state.list.page = action.payload.page; + state.list.page_size = action.payload.page_size; + }) + .addCase(fetchChatHistory.rejected, (state, action) => { + state.list.status = 'failed'; + state.list.error = action.payload || action.error.message; + }) + + // 创建聊天 + .addCase(createChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(createChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + state.list.items.unshift(action.payload); + state.list.total += 1; + state.currentChat.data = action.payload; + state.currentChat.status = 'succeeded'; + }) + .addCase(createChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 删除聊天 + .addCase(deleteChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(deleteChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + state.list.items = state.list.items.filter((chat) => chat.id !== action.payload); + state.list.total -= 1; + if (state.currentChat.data && state.currentChat.data.id === action.payload) { + state.currentChat.data = null; + } + }) + .addCase(deleteChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 更新聊天 + .addCase(updateChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(updateChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + const index = state.list.items.findIndex((chat) => chat.id === action.payload.id); + if (index !== -1) { + state.list.items[index] = action.payload; + } + if (state.currentChat.data && state.currentChat.data.id === action.payload.id) { + state.currentChat.data = action.payload; + } + }) + .addCase(updateChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }); + }, +}); + +// 导出 actions +export const { resetOperationStatus, resetCurrentChat, setCurrentChat } = chatHistorySlice.actions; + +// 导出 reducer +export default chatHistorySlice.reducer; diff --git a/src/store/chatHistory/chatHistory.thunks.js b/src/store/chatHistory/chatHistory.thunks.js new file mode 100644 index 0000000..679d87f --- /dev/null +++ b/src/store/chatHistory/chatHistory.thunks.js @@ -0,0 +1,87 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put, del } from '../../services/api'; + +/** + * 获取聊天历史列表 + * @param {Object} params - 查询参数 + * @param {number} params.page - 页码 + * @param {number} params.page_size - 每页数量 + */ +export const fetchChatHistory = createAsyncThunk( + 'chatHistory/fetchChatHistory', + async (params = { page: 1, page_size: 10 }, { rejectWithValue }) => { + try { + const response = await get('/chat-history/', { params }); + + // 处理返回格式 + if (response.data && response.data.code === 200) { + return response.data.data; + } + + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to fetch chat history'); + } + } +); + +/** + * 创建新聊天 + * @param {Object} chatData - 聊天数据 + * @param {string} chatData.knowledge_base_id - 知识库ID + * @param {string} chatData.title - 聊天标题 + */ +export const createChat = createAsyncThunk('chatHistory/createChat', async (chatData, { rejectWithValue }) => { + try { + const response = await post('/chat-history/', chatData); + + // 处理返回格式 + if (response.data && response.data.code === 200) { + return response.data.data.chat; + } + + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to create chat'); + } +}); + +/** + * 更新聊天 + * @param {Object} params - 更新参数 + * @param {string} params.id - 聊天ID + * @param {Object} params.data - 更新数据 + */ +export const updateChat = createAsyncThunk('chatHistory/updateChat', async ({ id, data }, { rejectWithValue }) => { + try { + const response = await put(`/chat-history/${id}/`, data); + + // 处理返回格式 + if (response.data && response.data.code === 200) { + return response.data.data.chat; + } + + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to update chat'); + } +}); + +/** + * 删除聊天 + * @param {string} id - 聊天ID + */ +export const deleteChat = createAsyncThunk('chatHistory/deleteChat', async (id, { rejectWithValue }) => { + try { + const response = await del(`/chat-history/${id}/`); + + // 处理返回格式 + if (response.data && response.data.code === 200) { + return id; + } + + return id; + } catch (error) { + return rejectWithValue(error.response?.data || 'Failed to delete chat'); + } +}); diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js index e961002..c48f17a 100644 --- a/src/store/knowledgeBase/knowledgeBase.thunks.js +++ b/src/store/knowledgeBase/knowledgeBase.thunks.js @@ -50,6 +50,12 @@ export const createKnowledgeBase = createAsyncThunk( async (knowledgeBaseData, { rejectWithValue }) => { try { const response = await post('/knowledge-bases/', knowledgeBaseData); + + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data.knowledge_base; + } + return response.data; } catch (error) { return rejectWithValue(error.response?.data || 'Failed to create knowledge base'); @@ -83,6 +89,12 @@ export const updateKnowledgeBase = createAsyncThunk( async ({ id, data }, { rejectWithValue }) => { try { const response = await put(`/knowledge-bases/${id}/`, data); + + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data.knowledge_base; + } + return response.data; } catch (error) { return rejectWithValue(error.response?.data || 'Failed to update knowledge base'); diff --git a/src/store/store.js b/src/store/store.js index f83d727..294b614 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -4,11 +4,13 @@ import sessionStorage from 'redux-persist/lib/storage/session'; import notificationReducer from './notification.slice.js'; import authReducer from './auth/auth.slice.js'; import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js'; +import chatReducer from './chat/chat.slice.js'; const rootRducer = combineReducers({ auth: authReducer, notification: notificationReducer, knowledgeBase: knowledgeBaseReducer, + chat: chatReducer, }); const persistConfig = { From 6cf31165f9e5a59ab5f6f8e5f4b655299dfb00f4 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Wed, 12 Mar 2025 22:24:05 -0400 Subject: [PATCH 11/21] [dev]add permission page --- src/layouts/HeaderWithNav.jsx | 18 +- src/pages/Permissions/PermissionsPage.jsx | 44 +++ .../components/PendingRequests.css | 37 +++ .../components/PendingRequests.jsx | 292 ++++++++++++++++++ .../components/UserPermissionDetails.jsx | 182 +++++++++++ .../components/UserPermissions.jsx | 206 ++++++++++++ src/router/router.jsx | 13 + src/services/permissionService.js | 55 ++++ src/store/auth/auth.mock.js | 13 - src/store/auth/auth.slice.js | 3 +- src/store/auth/auth.thunk.js | 3 +- src/store/chatHistory/chatHistory.mock.js | 153 --------- src/store/chatHistory/chatHistory.slice.js | 130 -------- src/store/chatHistory/chatHistory.thunks.js | 87 ------ src/store/permissions/permissions.slice.js | 83 +++++ src/store/permissions/permissions.thunks.js | 48 +++ src/store/store.js | 2 + 17 files changed, 982 insertions(+), 387 deletions(-) create mode 100644 src/pages/Permissions/PermissionsPage.jsx create mode 100644 src/pages/Permissions/components/PendingRequests.css create mode 100644 src/pages/Permissions/components/PendingRequests.jsx create mode 100644 src/pages/Permissions/components/UserPermissionDetails.jsx create mode 100644 src/pages/Permissions/components/UserPermissions.jsx create mode 100644 src/services/permissionService.js delete mode 100644 src/store/auth/auth.mock.js delete mode 100644 src/store/chatHistory/chatHistory.mock.js delete mode 100644 src/store/chatHistory/chatHistory.slice.js delete mode 100644 src/store/chatHistory/chatHistory.thunks.js create mode 100644 src/store/permissions/permissions.slice.js create mode 100644 src/store/permissions/permissions.thunks.js diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx index 3ee1685..9c9751a 100644 --- a/src/layouts/HeaderWithNav.jsx +++ b/src/layouts/HeaderWithNav.jsx @@ -8,6 +8,7 @@ export default function HeaderWithNav() { const navigate = useNavigate(); const location = useLocation(); const { user } = useSelector((state) => state.auth); + console.log('user', user); const handleLogout = async () => { try { @@ -22,6 +23,9 @@ export default function HeaderWithNav() { return location.pathname.startsWith(path); }; + // 检查用户是否有管理权限(leader 或 admin) + const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin'); + return (