From b4a0874a4dbeca327b5377f152be71420b0bbbea Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Wed, 19 Mar 2025 13:06:52 -0400 Subject: [PATCH] [dev]knowledgebase mock data & pending requests --- src/App.jsx | 8 +- src/components/ApiModeSwitch.jsx | 83 +++ src/layouts/HeaderWithNav.jsx | 1 - src/main.jsx | 4 +- src/pages/KnowledgeBase/KnowledgeBase.jsx | 9 +- src/pages/Permissions/Permissions.css | 39 ++ src/pages/Permissions/PermissionsPage.jsx | 39 +- .../components/PendingRequests.css | 214 ++++++ .../components/PendingRequests.jsx | 533 +++++++++++--- .../components/UserPermissionDetails.jsx | 156 ++++- .../components/UserPermissions.css | 210 ++++++ .../components/UserPermissions.jsx | 389 +++++++++-- src/services/api.js | 167 ++++- src/services/mockApi.js | 659 +++++++++++++++--- .../knowledgeBase/knowledgeBase.slice.js | 5 +- .../knowledgeBase/knowledgeBase.thunks.js | 6 +- 16 files changed, 2221 insertions(+), 301 deletions(-) create mode 100644 src/components/ApiModeSwitch.jsx create mode 100644 src/pages/Permissions/Permissions.css create mode 100644 src/pages/Permissions/components/UserPermissions.css diff --git a/src/App.jsx b/src/App.jsx index bdcd35e..0c41c6e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,8 +19,12 @@ function App() { console.log('app handleCheckAuth'); try { await dispatch(checkAuthThunk()).unwrap(); - if (user) navigate('/'); - } catch (error) {} + console.log('user', user); + if (!user) navigate('/login'); + } catch (error) { + console.log('error', error); + navigate('/login'); + } }; return ; diff --git a/src/components/ApiModeSwitch.jsx b/src/components/ApiModeSwitch.jsx new file mode 100644 index 0000000..1dbc4a6 --- /dev/null +++ b/src/components/ApiModeSwitch.jsx @@ -0,0 +1,83 @@ +import React, { useState, useEffect } from 'react'; +import { switchToMockApi, switchToRealApi, checkServerStatus } from '../services/api'; + +export default function ApiModeSwitch() { + const [isMockMode, setIsMockMode] = useState(false); + const [isChecking, setIsChecking] = useState(false); + const [showNotification, setShowNotification] = useState(false); + const [notification, setNotification] = useState({ message: '', type: 'info' }); + + // 组件加载时检查服务器状态 + useEffect(() => { + const checkStatus = async () => { + setIsChecking(true); + const isServerUp = await checkServerStatus(); + setIsMockMode(!isServerUp); + setIsChecking(false); + }; + + checkStatus(); + }, []); + + // 切换API模式 + const handleToggleMode = async () => { + setIsChecking(true); + + if (isMockMode) { + // 尝试切换回真实API + const isServerUp = await switchToRealApi(); + if (isServerUp) { + setIsMockMode(false); + showNotificationMessage('已切换到真实API模式', 'success'); + } else { + showNotificationMessage('服务器连接失败,继续使用模拟数据', 'warning'); + } + } else { + // 切换到模拟API + switchToMockApi(); + setIsMockMode(true); + showNotificationMessage('已切换到模拟API模式', 'info'); + } + + setIsChecking(false); + }; + + // 显示通知消息 + const showNotificationMessage = (message, type) => { + setNotification({ message, type }); + setShowNotification(true); + + // 3秒后自动隐藏通知 + setTimeout(() => { + setShowNotification(false); + }, 3000); + }; + + return ( +
+
+
+ + +
+ {isMockMode && 使用本地模拟数据} + {!isMockMode && 已连接到后端服务器} +
+ + {showNotification && ( +
+ {notification.message} +
+ )} +
+ ); +} diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx index 9c9751a..4e31724 100644 --- a/src/layouts/HeaderWithNav.jsx +++ b/src/layouts/HeaderWithNav.jsx @@ -8,7 +8,6 @@ export default function HeaderWithNav() { const navigate = useNavigate(); const location = useLocation(); const { user } = useSelector((state) => state.auth); - console.log('user', user); const handleLogout = async () => { try { diff --git a/src/main.jsx b/src/main.jsx index dbe6730..8203d0a 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -10,7 +10,7 @@ import { PersistGate } from 'redux-persist/integration/react'; import Loading from './components/Loading.jsx'; createRoot(document.getElementById('root')).render( - + // } persistor={persistor}> @@ -18,5 +18,5 @@ createRoot(document.getElementById('root')).render( - + // ); diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx index 4f983df..37a8a9e 100644 --- a/src/pages/KnowledgeBase/KnowledgeBase.jsx +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -52,10 +52,9 @@ export default function KnowledgeBase() { }); // Get knowledge bases from Redux store - const { items: knowledgeBases, total, status, error } = useSelector((state) => state.knowledgeBase.list); + const { data, status, error } = useSelector((state) => state.knowledgeBase.list); const { - items: searchResults, - total: searchTotal, + data: searchData, status: searchStatus, error: searchError, keyword: storeKeyword, @@ -63,8 +62,8 @@ export default function KnowledgeBase() { 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 displayData = isSearching ? searchData?.items : data?.items; + const displayTotal = isSearching ? searchData?.total : data?.total; const displayStatus = isSearching ? searchStatus : status; const displayError = isSearching ? searchError : error; diff --git a/src/pages/Permissions/Permissions.css b/src/pages/Permissions/Permissions.css new file mode 100644 index 0000000..5fa7f2d --- /dev/null +++ b/src/pages/Permissions/Permissions.css @@ -0,0 +1,39 @@ +.permissions-container { + padding: 24px; + background-color: #f8f9fa; + min-height: calc(100vh - 64px); +} + +.permissions-section { + background-color: #fff; + border-radius: 8px; + padding: 24px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +.api-mode-control { + background-color: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +.api-mode-control .api-mode-switch { + display: flex; + flex-direction: column; +} + +@media (max-width: 768px) { + .permissions-container { + padding: 16px; + } + + .permissions-section, + .api-mode-control { + padding: 16px; + } +} + +.permissions-section:last-child { + margin-bottom: 0; +} \ No newline at end of file diff --git a/src/pages/Permissions/PermissionsPage.jsx b/src/pages/Permissions/PermissionsPage.jsx index c9028cc..12fa25c 100644 --- a/src/pages/Permissions/PermissionsPage.jsx +++ b/src/pages/Permissions/PermissionsPage.jsx @@ -1,13 +1,14 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import PendingRequests from './components/PendingRequests'; import UserPermissions from './components/UserPermissions'; +import ApiModeSwitch from '../../components/ApiModeSwitch'; +import './Permissions.css'; export default function PermissionsPage() { const navigate = useNavigate(); const { user } = useSelector((state) => state.auth); - const [activeTab, setActiveTab] = useState('pending'); // 检查用户是否有管理权限(leader 或 admin) useEffect(() => { @@ -17,28 +18,18 @@ export default function PermissionsPage() { }, [user, navigate]); return ( -
- - - -
{activeTab === 'pending' ? : }
+
+
+ +
+ +
+ +
+ +
+ +
); } diff --git a/src/pages/Permissions/components/PendingRequests.css b/src/pages/Permissions/components/PendingRequests.css index 976cfb7..9ddbd81 100644 --- a/src/pages/Permissions/components/PendingRequests.css +++ b/src/pages/Permissions/components/PendingRequests.css @@ -34,4 +34,218 @@ .badge.bg-secondary { background-color: #6c757d !important; +} + +/* 表格行鼠标样式 */ +.cursor-pointer { + cursor: pointer; +} + +/* 滑动面板样式 */ +.slide-over-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1040; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.slide-over-backdrop.show { + opacity: 1; + visibility: visible; +} + +.slide-over { + position: fixed; + top: 0; + right: -450px; + width: 450px; + height: 100%; + background-color: #fff; + z-index: 1050; + transition: right 0.3s ease; + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1); +} + +.slide-over.show { + right: 0; +} + +.slide-over-content { + display: flex; + flex-direction: column; + height: 100%; +} + +.slide-over-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #e9ecef; +} + +.slide-over-body { + flex: 1; + padding: 1rem; + overflow-y: auto; +} + +.slide-over-footer { + padding: 1rem; + border-top: 1px solid #e9ecef; + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} + +/* 头像占位符 */ +.avatar-placeholder { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #6c757d; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; +} + +/* 新增样式 - 白色基调 */ +.badge-count { + background-color: #ff4d4f; + color: white; + border-radius: 20px; + padding: 4px 12px; + font-size: 14px; +} + +.pending-requests-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.pending-request-item { + position: relative; + background-color: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.2s ease; +} + +.pending-request-item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.request-header { + display: flex; + justify-content: space-between; + margin-bottom: 12px; +} + +.user-info h6 { + font-weight: 600; +} + +.department { + color: #666; + font-size: 14px; + margin: 0; +} + +.request-date { + color: #999; + font-size: 14px; +} + +.request-content { + color: #333; +} + +.permission-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.permission-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.permission-badge.read { + background-color: #e6f7ff; + color: #1890ff; + border: 1px solid #91d5ff; +} + +.permission-badge.edit { + background-color: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; +} + +.permission-badge.delete { + background-color: #fff2f0; + color: #ff4d4f; + border: 1px solid #ffccc7; +} + +.request-actions { + position: absolute; + right: 1rem; + bottom: 1rem; + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} + + +/* 分页控件样式 */ +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; + padding: 10px 0; +} + +.pagination-button { + background-color: #fff; + border: 1px solid #d9d9d9; + color: #333; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.pagination-button:hover:not(:disabled) { + border-color: #1890ff; + color: #1890ff; +} + +.pagination-button:disabled { + color: #d9d9d9; + cursor: not-allowed; +} + +.pagination-info { + margin: 0 15px; + color: #666; + font-size: 14px; } \ No newline at end of file diff --git a/src/pages/Permissions/components/PendingRequests.jsx b/src/pages/Permissions/components/PendingRequests.jsx index f222d62..5025dd2 100644 --- a/src/pages/Permissions/components/PendingRequests.jsx +++ b/src/pages/Permissions/components/PendingRequests.jsx @@ -8,6 +8,140 @@ import { } from '../../../store/permissions/permissions.thunks'; import { resetApproveRejectStatus } from '../../../store/permissions/permissions.slice'; import './PendingRequests.css'; // 引入外部CSS文件 +import SvgIcon from '../../../components/SvgIcon'; + +// 模拟数据 +const mockPendingRequests = [ + { + id: 1, + applicant: { + name: '王五', + department: '达人组', + }, + knowledge_base: { + name: '达人直播数据报告', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: false, + }, + reason: '需要查看和编辑直播数据报告', + created_at: '2024-01-07T10:30:00Z', + expires_at: null, + }, + { + id: 2, + applicant: { + name: '赵六', + department: '直播组', + }, + knowledge_base: { + name: '人力资源政策文件', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要了解最新的人力资源政策', + created_at: '2024-01-06T14:20:00Z', + expires_at: '2025-01-06T14:20:00Z', + }, + { + id: 3, + applicant: { + name: '钱七', + department: '市场部', + }, + knowledge_base: { + name: '市场分析报告', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要了解市场趋势', + created_at: '2024-01-05T09:15:00Z', + expires_at: '2024-07-05T09:15:00Z', + }, + { + id: 4, + applicant: { + name: '孙八', + department: '技术部', + }, + knowledge_base: { + name: '技术架构文档', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: true, + }, + reason: '需要进行技术架构更新', + created_at: '2024-01-04T16:45:00Z', + expires_at: null, + }, + { + id: 5, + applicant: { + name: '周九', + department: '产品部', + }, + knowledge_base: { + name: '产品规划文档', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: false, + }, + reason: '需要参与产品规划讨论', + created_at: '2024-01-03T11:30:00Z', + expires_at: '2024-12-31T23:59:59Z', + }, + { + id: 6, + applicant: { + name: '吴十', + department: '设计部', + }, + knowledge_base: { + name: '设计规范文档', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要参考设计规范', + created_at: '2024-01-02T14:20:00Z', + expires_at: '2024-06-30T23:59:59Z', + }, + { + id: 7, + applicant: { + name: '郑十一', + department: '财务部', + }, + knowledge_base: { + name: '财务报表', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要查看财务数据', + created_at: '2024-01-01T09:00:00Z', + expires_at: null, + }, +]; + +// 每页显示的申请数量 +const PAGE_SIZE = 5; export default function PendingRequests() { const dispatch = useDispatch(); @@ -15,14 +149,19 @@ export default function PendingRequests() { const [showResponseInput, setShowResponseInput] = useState(false); const [currentRequestId, setCurrentRequestId] = useState(null); const [isApproving, setIsApproving] = useState(false); + const [selectedRequest, setSelectedRequest] = useState(null); + const [showSlideOver, setShowSlideOver] = useState(false); - // 从Redux store获取权限申请列表和状态 - const { - items: pendingRequests, - status: fetchStatus, - error: fetchError, - } = useSelector((state) => state.permissions.permissions); + // 使用本地状态管理待处理申请列表 + const [pendingRequests, setPendingRequests] = useState([]); + const [fetchStatus, setFetchStatus] = useState('idle'); + const [fetchError, setFetchError] = useState(null); + // 分页状态 + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + // 从Redux store获取批准/拒绝操作的状态 const { status: approveRejectStatus, error: approveRejectError, @@ -31,7 +170,38 @@ export default function PendingRequests() { // 获取待处理申请列表 useEffect(() => { - dispatch(fetchPermissionsThunk()); + const fetchData = async () => { + try { + setFetchStatus('loading'); + // 尝试从API获取数据 + const result = await dispatch(fetchPermissionsThunk()); + + // 检查API返回的数据 + if (result && result.payload && result.payload.length > 0) { + // 使用API返回的数据 + setPendingRequests(result.payload); + setTotalPages(Math.ceil(result.payload.length / PAGE_SIZE)); + console.log('使用API返回的待处理申请数据'); + } else { + // API返回的数据为空,使用模拟数据 + console.log('API返回的待处理申请数据为空,使用模拟数据'); + setPendingRequests(mockPendingRequests); + setTotalPages(Math.ceil(mockPendingRequests.length / PAGE_SIZE)); + } + setFetchStatus('succeeded'); + } catch (error) { + console.error('获取待处理申请失败:', error); + setFetchError('获取待处理申请失败'); + setFetchStatus('failed'); + + // API请求失败,使用模拟数据作为后备 + console.log('API请求失败,使用模拟数据作为后备'); + setPendingRequests(mockPendingRequests); + setTotalPages(Math.ceil(mockPendingRequests.length / PAGE_SIZE)); + } + }; + + fetchData(); }, [dispatch]); // 监听批准/拒绝操作的状态变化 @@ -46,6 +216,21 @@ export default function PendingRequests() { setShowResponseInput(false); setCurrentRequestId(null); setResponseMessage(''); + setShowSlideOver(false); + setSelectedRequest(null); + + // 从列表中移除已处理的申请 + const updatedRequests = pendingRequests.filter((req) => req.id !== currentRequestId); + setPendingRequests(updatedRequests); + + // 更新总页数 + setTotalPages(Math.ceil(updatedRequests.length / PAGE_SIZE)); + + // 如果当前页没有数据了,且不是第一页,则回到上一页 + if (getCurrentPageData().length === 1 && currentPage > 1) { + setCurrentPage(currentPage - 1); + } + // 重置状态 dispatch(resetApproveRejectStatus()); } else if (approveRejectStatus === 'failed') { @@ -58,7 +243,15 @@ export default function PendingRequests() { // 重置状态 dispatch(resetApproveRejectStatus()); } - }, [approveRejectStatus, approveRejectError, dispatch, isApproving]); + }, [ + approveRejectStatus, + approveRejectError, + dispatch, + isApproving, + currentRequestId, + pendingRequests, + currentPage, + ]); // 打开回复输入框 const handleOpenResponseInput = (requestId, approving) => { @@ -90,19 +283,80 @@ export default function PendingRequests() { } }; - // 获取权限类型文本和样式 - const getPermissionTypeText = (permissions) => { - if (permissions.can_read && !permissions.can_edit) { - return '只读访问'; - } else if (permissions.can_edit) { - return '完全访问'; + // 处理行点击,显示滑动面板 + const handleRowClick = (request) => { + setSelectedRequest(request); + setShowSlideOver(true); + }; + + // 关闭滑动面板 + const handleCloseSlideOver = () => { + setShowSlideOver(false); + setTimeout(() => { + setSelectedRequest(null); + }, 300); // 等待动画结束后再清除选中的申请 + }; + + // 直接处理申请(不显示弹窗) + const handleDirectProcess = (requestId, approve) => { + setCurrentRequestId(requestId); + setIsApproving(approve); + + const params = { + id: requestId, + responseMessage: approve ? '已批准' : '已拒绝', + }; + + if (approve) { + dispatch(approvePermissionThunk(params)); } else { - return '未知权限'; + dispatch(rejectPermissionThunk(params)); } }; + // 获取当前页的数据 + const getCurrentPageData = () => { + const startIndex = (currentPage - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + return pendingRequests.slice(startIndex, endIndex); + }; + + // 处理页码变化 + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + // 渲染分页控件 + const renderPagination = () => { + if (totalPages <= 1) return null; + + return ( +
+ + +
+ {currentPage} / {totalPages} +
+ + +
+ ); + }; + // 渲染加载状态 - if (fetchStatus === 'loading') { + if (fetchStatus === 'loading' && pendingRequests.length === 0) { return (
@@ -114,7 +368,7 @@ export default function PendingRequests() { } // 渲染错误状态 - if (fetchStatus === 'failed') { + if (fetchStatus === 'failed' && pendingRequests.length === 0) { return (
{fetchError || '获取待处理申请失败'} @@ -131,95 +385,196 @@ export default function PendingRequests() { ); } + // 获取当前页的数据 + const currentPageData = getCurrentPageData(); + // 渲染申请列表 return ( <> -
- {/*
待处理申请
*/} -
{pendingRequests.length}个待处理
+
+
待处理申请
+
{pendingRequests.length}个待处理
-
- {pendingRequests.map((request) => ( -
-
-
-
-
{request.applicant.name || request.applicant}
-

{request.applicant.department || '无归属部门'}

-
-
-
- {new Date(request.created_at).toLocaleDateString()} +
+ {currentPageData.map((request) => ( +
handleRowClick(request)}> +
+
+
{request.applicant.name}
+

{request.applicant.department}

+
+
{new Date(request.created_at).toLocaleDateString()}
+
+ +
+

申请访问:{request.knowledge_base.name}

+ + {request.permissions.can_edit ? ( + + + 完全访问 + + ) : ( + request.permissions.can_read && ( + + + 只读访问 + + ) + )} +
+
+ + +
+
+ ))} +
+ + {/* 分页控件 */} + {renderPagination()} + + {/* 滑动面板 */} +
+
+ {selectedRequest && ( +
+
+
申请详情
+ +
+
+
+
申请人信息
+
+
+ {(selectedRequest.applicant.name || selectedRequest.applicant).charAt(0)} +
+
+
+ {selectedRequest.applicant.name || selectedRequest.applicant} +
+

+ {selectedRequest.applicant.department || '无归属部门'} +

-
-
申请访问:{request.knowledge_base.name || request.knowledge_base}
-
- {request.permissions.can_read && ( - 只读 +
+
知识库信息
+

+ 名称:{' '} + {selectedRequest.knowledge_base.name || selectedRequest.knowledge_base} +

+
+ +
+
申请权限
+
+ {selectedRequest.permissions.can_read && !selectedRequest.permissions.can_edit && ( + + 只读 + )} - {request.permissions.can_edit && ( - 编辑 - )} - {request.permissions.can_delete && ( - 删除 - )} - {request.expires_at && ( - - 至 {new Date(request.expires_at).toLocaleDateString()} + {selectedRequest.permissions.can_edit && selectedRequest.permissions.can_delete && ( + + 完全访问 )}
-
- 申请理由: {request.reason} -
-
- - + +
+
申请时间
+

{new Date(selectedRequest.created_at).toLocaleString()}

+
+ + {selectedRequest.expires_at && ( +
+
到期时间
+

{new Date(selectedRequest.expires_at).toLocaleString()}

+
+ )} + +
+
申请理由
+
{selectedRequest.reason || '无申请理由'}
+
+ + +
- ))} + )}
{/* 回复输入弹窗 */} diff --git a/src/pages/Permissions/components/UserPermissionDetails.jsx b/src/pages/Permissions/components/UserPermissionDetails.jsx index cd57416..6015a0b 100644 --- a/src/pages/Permissions/components/UserPermissionDetails.jsx +++ b/src/pages/Permissions/components/UserPermissionDetails.jsx @@ -1,6 +1,62 @@ import React, { useState, useEffect } from 'react'; import { get } from '../../../services/api'; +// 模拟用户权限数据 +const mockUserPermissions = [ + { + knowledge_base: { + id: '1', + name: '达人直播数据报告', + department: '达人组', + }, + permission: { + can_read: true, + can_edit: true, + can_admin: false, + }, + last_access_time: '2024-03-10T14:30:00Z', + }, + { + knowledge_base: { + id: '2', + name: '人力资源政策文件', + department: '人力资源组', + }, + permission: { + can_read: true, + can_edit: false, + can_admin: false, + }, + last_access_time: '2024-03-08T09:15:00Z', + }, + { + knowledge_base: { + id: '3', + name: '市场分析报告', + department: '市场部', + }, + permission: { + can_read: true, + can_edit: false, + can_admin: false, + }, + last_access_time: null, + }, + { + knowledge_base: { + id: '4', + name: '产品规划文档', + department: '产品部', + }, + permission: { + can_read: true, + can_edit: true, + can_admin: true, + }, + last_access_time: '2024-03-15T11:20:00Z', + }, +]; + export default function UserPermissionDetails({ user, onClose, onSave }) { const [userPermissions, setUserPermissions] = useState([]); const [loading, setLoading] = useState(true); @@ -15,13 +71,29 @@ export default function UserPermissionDetails({ user, onClose, onSave }) { const response = await get(`/users/${user.id}/permissions/`); if (response && response.code === 200) { - setUserPermissions(response.data.permissions || []); + // 检查API返回的数据 + const apiPermissions = response.data.permissions || []; + if (apiPermissions.length > 0) { + // 使用API返回的数据 + setUserPermissions(apiPermissions); + console.log('使用API返回的用户权限数据'); + } else { + // API返回的数据为空,使用模拟数据 + console.log('API返回的用户权限数据为空,使用模拟数据'); + setUserPermissions(mockUserPermissions); + } } else { - setError('获取用户权限详情失败'); + // API请求失败,使用模拟数据 + console.log('API请求失败,使用模拟数据'); + setUserPermissions(mockUserPermissions); + setError('获取用户权限详情失败,显示模拟数据'); } } catch (error) { console.error('获取用户权限详情失败:', error); - setError('获取用户权限详情失败'); + // API请求出错,使用模拟数据作为后备 + console.log('API请求出错,使用模拟数据作为后备'); + setUserPermissions(mockUserPermissions); + setError('获取用户权限详情失败,显示模拟数据'); } finally { setLoading(false); } @@ -72,7 +144,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) { return (
-
+
{user.name} 的权限详情
@@ -87,8 +159,76 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {

加载权限详情...

) : error ? ( -
- {error} +
+
+ {error} +
+ {userPermissions.length > 0 && ( +
+ + + + + + + + + + + + {userPermissions.map((item) => { + const currentPermissionType = getPermissionType(item.permission); + const updatedPermissionType = + updatedPermissions[item.knowledge_base.id] || currentPermissionType; + + return ( + + + + + + + + ); + })} + +
知识库名称所属部门当前权限最后访问时间操作
{item.knowledge_base.name}{item.knowledge_base.department || '未指定'} + + {getPermissionTypeText(currentPermissionType)} + + + {item.last_access_time + ? new Date(item.last_access_time).toLocaleString() + : '从未访问'} + + +
+
+ )}
) : userPermissions.length === 0 ? (
@@ -167,7 +307,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
-
+
); } diff --git a/src/pages/Permissions/components/UserPermissions.css b/src/pages/Permissions/components/UserPermissions.css new file mode 100644 index 0000000..1e1dd18 --- /dev/null +++ b/src/pages/Permissions/components/UserPermissions.css @@ -0,0 +1,210 @@ +.search-box { + position: relative; +} + +.search-input { + padding: 8px 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + width: 240px; + font-size: 14px; + outline: none; + transition: all 0.3s; +} + +.search-input:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + +.user-permissions-table { + background-color: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.table-header { + display: flex; + background-color: #fafafa; + border-bottom: 1px solid #f0f0f0; + padding: 12px 16px; + font-weight: 600; + color: #333; +} + +.header-cell { + flex: 1; +} + +.header-cell:first-child { + flex: 1.5; +} + +.table-row { + display: flex; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + transition: background-color 0.3s; +} + +.table-row:hover { + background-color: #fafafa; +} + +.table-row:last-child { + border-bottom: none; +} + +.cell { + flex: 1; + display: flex; + align-items: center; +} + +.cell:first-child { + flex: 1.5; +} + +.user-cell { + display: flex; + align-items: center; + gap: 12px; +} + +.avatar-placeholder { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: #1890ff; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 16px; +} + +.user-info { + display: flex; + flex-direction: column; +} + +.user-name { + font-weight: 500; + color: #333; +} + +.user-username { + font-size: 12px; + color: #999; +} + +.permission-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.permission-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.permission-badge.read { + background-color: #e6f7ff; + color: #1890ff; + border: 1px solid #91d5ff; +} + +.permission-badge.edit { + background-color: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; +} + +.permission-badge.admin { + background-color: #fff2f0; + color: #ff4d4f; + border: 1px solid #ffccc7; +} + +.action-cell { + justify-content: flex-end; +} + +.btn-details { + background-color: transparent; + border: 1px solid #1890ff; + color: #1890ff; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.btn-details:hover { + background-color: #e6f7ff; +} + +/* 分页控件样式 */ +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; + padding: 10px 0; +} + +.pagination-button { + background-color: #fff; + border: 1px solid #d9d9d9; + color: #333; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.pagination-button:hover:not(:disabled) { + border-color: #1890ff; + color: #1890ff; +} + +.pagination-button:disabled { + color: #d9d9d9; + cursor: not-allowed; +} + +.pagination-info { + margin: 0 15px; + color: #666; + font-size: 14px; +} + +/* 页面大小选择器样式 */ +.page-size-selector { + margin-left: 20px; + display: flex; + align-items: center; +} + +.page-size-select { + padding: 5px 10px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background-color: #fff; + font-size: 14px; + color: #333; + cursor: pointer; + outline: none; + transition: all 0.3s; +} + +.page-size-select:hover, .page-size-select:focus { + border-color: #1890ff; +} \ No newline at end of file diff --git a/src/pages/Permissions/components/UserPermissions.jsx b/src/pages/Permissions/components/UserPermissions.jsx index dc6d8d5..75b9ad0 100644 --- a/src/pages/Permissions/components/UserPermissions.jsx +++ b/src/pages/Permissions/components/UserPermissions.jsx @@ -3,6 +3,111 @@ import { useDispatch } from 'react-redux'; import { get, put } from '../../../services/api'; import { showNotification } from '../../../store/notification.slice'; import UserPermissionDetails from './UserPermissionDetails'; +import './UserPermissions.css'; +import SvgIcon from '../../../components/SvgIcon'; + +// 模拟数据 +const mockUsers = [ + { + id: '1', + username: 'zhangsan', + name: '张三', + department: '达人组', + position: '达人对接', + permissions_count: { + read: 1, + edit: 0, + admin: 0, + }, + }, + { + id: '2', + username: 'lisi', + name: '李四', + department: '人力资源组', + position: 'HR', + permissions_count: { + read: 1, + edit: 0, + admin: 2, + }, + }, + { + id: '3', + username: 'wangwu', + name: '王五', + department: '市场部', + position: '市场专员', + permissions_count: { + read: 2, + edit: 1, + admin: 0, + }, + }, + { + id: '4', + username: 'zhaoliu', + name: '赵六', + department: '技术部', + position: '前端开发', + permissions_count: { + read: 3, + edit: 2, + admin: 1, + }, + }, + { + id: '5', + username: 'sunqi', + name: '孙七', + department: '产品部', + position: '产品经理', + permissions_count: { + read: 4, + edit: 2, + admin: 0, + }, + }, + { + id: '6', + username: 'zhouba', + name: '周八', + department: '设计部', + position: 'UI设计师', + permissions_count: { + read: 1, + edit: 1, + admin: 0, + }, + }, + { + id: '7', + username: 'wujiu', + name: '吴九', + department: '财务部', + position: '财务主管', + permissions_count: { + read: 2, + edit: 0, + admin: 3, + }, + }, + { + id: '8', + username: 'zhengshi', + name: '郑十', + department: '行政部', + position: '行政专员', + permissions_count: { + read: 1, + edit: 0, + admin: 0, + }, + }, +]; + +// 每页显示选项 +const PAGE_SIZE_OPTIONS = [5, 10, 15, 20]; export default function UserPermissions() { const dispatch = useDispatch(); @@ -11,6 +116,12 @@ export default function UserPermissions() { const [error, setError] = useState(null); const [selectedUser, setSelectedUser] = useState(null); const [showDetailsModal, setShowDetailsModal] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + + // 分页状态 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(5); + const [totalPages, setTotalPages] = useState(1); // 获取用户列表 useEffect(() => { @@ -20,13 +131,25 @@ export default function UserPermissions() { const response = await get('/users/permissions/'); if (response && response.code === 200) { - setUsers(response.data.users || []); + // 只有当API返回的用户列表为空时才使用模拟数据 + const apiUsers = response.data.users || []; + if (apiUsers.length > 0) { + setUsers(apiUsers); + } else { + console.log('API返回的用户列表为空,使用模拟数据'); + setUsers(mockUsers); + } } else { - setError('获取用户列表失败'); + // API请求失败,使用模拟数据 + console.log('API请求失败,使用模拟数据'); + setUsers(mockUsers); } } catch (error) { console.error('获取用户列表失败:', error); setError('获取用户列表失败'); + // API请求出错,使用模拟数据作为后备 + console.log('API请求出错,使用模拟数据作为后备'); + setUsers(mockUsers); } finally { setLoading(false); } @@ -35,6 +158,16 @@ export default function UserPermissions() { fetchUsers(); }, []); + // 当用户列表或搜索词变化时,更新总页数 + useEffect(() => { + const filteredUsers = getFilteredUsers(); + setTotalPages(Math.ceil(filteredUsers.length / pageSize)); + // 如果当前页超出了新的总页数,则重置为第一页 + if (currentPage > Math.ceil(filteredUsers.length / pageSize)) { + setCurrentPage(1); + } + }, [users, searchTerm, pageSize]); + // 打开用户权限详情弹窗 const handleOpenDetailsModal = (user) => { setSelectedUser(user); @@ -99,8 +232,99 @@ export default function UserPermissions() { } }; + // 处理搜索输入变化 + const handleSearchChange = (e) => { + setSearchTerm(e.target.value); + setCurrentPage(1); // 重置为第一页 + }; + + // 处理页码变化 + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + // 处理每页显示数量变化 + const handlePageSizeChange = (e) => { + const newPageSize = parseInt(e.target.value); + setPageSize(newPageSize); + setCurrentPage(1); // 重置为第一页 + }; + + // 获取过滤后的用户列表 + const getFilteredUsers = () => { + if (!searchTerm.trim()) return users; + + return users.filter( + (user) => + user.name.toLowerCase().includes(searchTerm.toLowerCase()) || + user.username.toLowerCase().includes(searchTerm.toLowerCase()) || + user.department.toLowerCase().includes(searchTerm.toLowerCase()) || + user.position.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }; + + // 获取当前页的数据 + const getCurrentPageData = () => { + const filteredUsers = getFilteredUsers(); + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + return filteredUsers.slice(startIndex, endIndex); + }; + + // 渲染分页控件 + const renderPagination = () => { + if (totalPages <= 1) return null; + + return ( +
+ + +
+ +
+
+ ); + }; + // 渲染加载状态 - if (loading) { + if (loading && users.length === 0) { return (
@@ -112,7 +336,7 @@ export default function UserPermissions() { } // 渲染错误状态 - if (error) { + if (error && users.length === 0) { return (
{error} @@ -129,70 +353,111 @@ export default function UserPermissions() { ); } + // 获取当前页的数据 + const currentPageData = getCurrentPageData(); + + // 获取过滤后的总用户数 + const filteredUsersCount = getFilteredUsers().length; + // 渲染用户列表 return ( <> -
-
-
用户权限管理
-
-
-
- - - - - - - - - - - - - {users.map((user) => ( - - - - - - - - - ))} - -
用户名姓名部门职位权限操作
{user.username}{user.name}{user.department}{user.position} - {user.permissions_count && ( -
- {user.permissions_count.read > 0 && ( - - 只读: {user.permissions_count.read} - - )} - {user.permissions_count.edit > 0 && ( - - 编辑: {user.permissions_count.edit} - - )} - {user.permissions_count.admin > 0 && ( - - 管理: {user.permissions_count.admin} - - )} -
- )} -
- -
-
+
+
用户权限管理
+
+ + + +
+ {filteredUsersCount > 0 ? ( + <> +
+
+ + + + + + + + + + + + {currentPageData.map((user) => ( + + + + + + + + ))} + +
用户部门职位数据集权限 + 操作 +
+
+
+ {user.name.charAt(0)} +
+
+
{user.name}
+
{user.username}
+
+
+
{user.department}{user.position} + {user.permissions_count && ( +
+ {user.permissions_count.read > 0 && ( + + 完全访问: {user.permissions_count.read} + + )} + {user.permissions_count.edit > 0 && ( + + 只读访问: {user.permissions_count.edit} + + )} + {user.permissions_count.admin > 0 && ( + + 无访问权限: {user.permissions_count.admin} + + )} +
+ )} +
+ +
+
+
+ + {/* 分页控件 */} + {renderPagination()} + + ) : ( +
+ 没有找到匹配的用户 +
+ )} + {/* 用户权限详情弹窗 */} {showDetailsModal && selectedUser && ( { + // 如果成功收到响应,表示服务器正常工作 + if (!hasCheckedServer) { + console.log('Server is up and running'); + isServerDown = false; + hasCheckedServer = true; + } return response; }, (error) => { + // 处理服务器无法连接的情况 + if (!error.response || error.code === 'ECONNABORTED' || error.message.includes('Network Error')) { + console.error('Server appears to be down. Switching to mock data.'); + isServerDown = true; + hasCheckedServer = true; + } + // Handle errors in the response if (error.response) { // monitor /verify @@ -56,45 +74,144 @@ api.interceptors.response.use( } ); -// Define common HTTP methods +// 检查服务器状态 +export const checkServerStatus = async () => { + try { + await api.get('/health-check', { timeout: 3000 }); + isServerDown = false; + hasCheckedServer = true; + console.log('Server connection established'); + return true; + } catch (error) { + isServerDown = true; + hasCheckedServer = true; + console.error('Server connection failed, using mock data'); + return false; + } +}; + +// 初始检查服务器状态 +checkServerStatus(); + +// Define common HTTP methods with fallback to mock API const get = async (url, params = {}) => { - const res = await api.get(url, { params }); - return res.data; + try { + if (isServerDown) { + console.log(`[MOCK MODE] GET ${url}`); + return await mockGet(url, params); + } + + const res = await api.get(url, { params }); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for GET ${url}`); + return await mockGet(url, params); + } + throw error; + } }; -// Handle POST requests for JSON data +// Handle POST requests for JSON data with fallback to mock API 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 + try { + if (isServerDown) { + console.log(`[MOCK MODE] POST ${url}`); + return await mockPost(url, data); + } - const res = await api.post(url, data, { headers }); - return res.data; + const headers = isMultipart + ? { 'Content-Type': 'multipart/form-data' } // For file uploads + : { 'Content-Type': 'application/json' }; // For JSON data + + const res = await api.post(url, data, { headers }); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for POST ${url}`); + return await mockPost(url, data); + } + throw error; + } }; -// Handle PUT requests +// Handle PUT requests with fallback to mock API const put = async (url, data) => { - const res = await api.put(url, data, { - headers: { 'Content-Type': 'application/json' }, - }); - return res.data; + try { + if (isServerDown) { + console.log(`[MOCK MODE] PUT ${url}`); + return await mockPut(url, data); + } + + const res = await api.put(url, data, { + headers: { 'Content-Type': 'application/json' }, + }); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for PUT ${url}`); + return await mockPut(url, data); + } + throw error; + } }; -// Handle DELETE requests +// Handle DELETE requests with fallback to mock API const del = async (url) => { - const res = await api.delete(url); - return res.data; + try { + if (isServerDown) { + console.log(`[MOCK MODE] DELETE ${url}`); + return await mockDelete(url); + } + + const res = await api.delete(url); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for DELETE ${url}`); + return await mockDelete(url); + } + throw error; + } }; const upload = async (url, data) => { - const axiosInstance = await axios.create({ - baseURL: '/api', - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - const res = await axiosInstance.post(url, data); - return res.data; + try { + if (isServerDown) { + console.log(`[MOCK MODE] Upload ${url}`); + return await mockPost(url, data, true); + } + + const axiosInstance = await axios.create({ + baseURL: '/api', + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + const res = await axiosInstance.post(url, data); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for Upload ${url}`); + return await mockPost(url, data, true); + } + throw error; + } +}; + +// 手动切换到模拟API(为调试目的) +export const switchToMockApi = () => { + isServerDown = true; + hasCheckedServer = true; + console.log('Manually switched to mock API'); +}; + +// 手动切换回真实API +export const switchToRealApi = async () => { + // 重新检查服务器状态 + const isServerUp = await checkServerStatus(); + console.log(isServerUp ? 'Switched back to real API' : 'Server still down, continuing with mock API'); + return isServerUp; }; export { get, post, put, del, upload }; diff --git a/src/services/mockApi.js b/src/services/mockApi.js index 38f358f..c2afe2f 100644 --- a/src/services/mockApi.js +++ b/src/services/mockApi.js @@ -4,75 +4,43 @@ import { v4 as uuidv4 } from 'uuid'; // Mock data for knowledge bases const mockKnowledgeBases = [ { - id: 'kb-001', + id: uuidv4(), + user_id: 'user-001', name: 'Frontend Development', - description: 'Resources and guides for frontend development including React, Vue, and Angular', - created_at: '2023-10-15T08:30:00Z', - updated_at: '2023-12-20T14:45:00Z', - create_time: '2023-10-15T08:30:00Z', - update_time: '2023-12-20T14:45:00Z', + desc: 'Resources and guides for frontend development including React, Vue, and Angular', type: 'private', department: '研发部', group: '前端开发组', - owner: { - id: 'user-001', - username: 'johndoe', - email: 'john@example.com', - department: '研发部', - group: '前端开发组', - }, - document_count: 15, - tags: ['react', 'javascript', 'frontend'], + documents: [], + char_length: 0, + document_count: 0, + external_id: uuidv4(), + create_time: '2024-02-26T08:30:00Z', + update_time: '2024-02-26T14:45:00Z', permissions: { - can_edit: true, can_read: true, + can_edit: true, + can_delete: false, }, - documents: [ - { - id: 'doc-001', - name: 'React Best Practices.pdf', - description: 'A guide to React best practices and patterns', - size: '1.2MB', - create_time: '2023-10-20T09:15:00Z', - update_time: '2023-10-20T09:15:00Z', - }, - { - id: 'doc-002', - name: 'Vue.js Tutorial.docx', - description: 'Step-by-step tutorial for Vue.js beginners', - size: '850KB', - create_time: '2023-11-05T14:30:00Z', - update_time: '2023-11-05T14:30:00Z', - }, - { - id: 'doc-003', - name: 'JavaScript ES6 Features.pdf', - description: 'Overview of ES6 features and examples', - size: '1.5MB', - create_time: '2023-11-15T11:45:00Z', - update_time: '2023-11-15T11:45:00Z', - }, - ], }, { - id: 'kb-002', + id: uuidv4(), + user_id: 'user-001', name: 'Backend Technologies', - description: 'Information about backend frameworks, databases, and server configurations', - created_at: '2023-09-05T10:15:00Z', - updated_at: '2024-01-10T09:20:00Z', - create_time: '2023-09-05T10:15:00Z', - update_time: '2024-01-10T09:20:00Z', + desc: 'Information about backend frameworks, databases, and server configurations', type: 'private', - owner: { - id: 'user-001', - username: 'johndoe', - email: 'john@example.com', - }, - document_count: 23, - tags: ['nodejs', 'python', 'databases'], + department: '研发部', + group: '后端开发组', + documents: [], + char_length: 0, + document_count: 0, + external_id: uuidv4(), + create_time: '2024-02-25T10:15:00Z', + update_time: '2024-02-26T09:20:00Z', permissions: { - can_edit: true, can_read: true, + can_edit: true, + can_delete: false, }, }, { @@ -206,6 +174,36 @@ const mockKnowledgeBases = [ // In-memory store for CRUD operations let knowledgeBases = [...mockKnowledgeBases]; +// Mock user data for authentication +const mockUsers = [ + { + id: 'user-001', + username: 'leader2', + password: 'leader123', // 在实际应用中不应该存储明文密码 + email: 'admin@example.com', + name: '管理员', + department: '研发部', + group: '前端开发组', + role: 'admin', + avatar: null, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + }, + { + id: 'user-002', + username: 'user', + password: 'user123', // 在实际应用中不应该存储明文密码 + email: 'user@example.com', + name: '普通用户', + department: '市场部', + group: '市场组', + role: 'user', + avatar: null, + created_at: '2024-01-02T00:00:00Z', + updated_at: '2024-01-02T00:00:00Z', + }, +]; + // Helper function for pagination const paginate = (array, page_size, page) => { const startIndex = (page - 1) * page_size; @@ -220,18 +218,314 @@ const paginate = (array, page_size, page) => { }; }; -// 导入聊天历史模拟数据和方法 -import { - mockChatHistory, - mockGetChatHistory, - mockCreateChat, - mockUpdateChat, - mockDeleteChat, -} from '../store/chatHistory/chatHistory.mock'; +// Mock chat history data +const mockChatHistory = [ + { + id: 'chat-001', + title: '关于React组件开发的问题', + knowledge_base_id: 'kb-001', + knowledge_base_name: 'Frontend Development', + message_count: 5, + created_at: '2024-03-15T10:30:00Z', + updated_at: '2024-03-15T11:45:00Z', + }, + { + id: 'chat-002', + title: 'Vue.js性能优化讨论', + knowledge_base_id: 'kb-001', + knowledge_base_name: 'Frontend Development', + message_count: 3, + created_at: '2024-03-14T15:20:00Z', + updated_at: '2024-03-14T16:10:00Z', + }, + { + id: 'chat-003', + title: '后端API集成问题', + knowledge_base_id: 'kb-002', + knowledge_base_name: 'Backend Technologies', + message_count: 4, + created_at: '2024-03-13T09:15:00Z', + updated_at: '2024-03-13T10:30:00Z', + }, +]; + +// Mock chat history functions +const mockGetChatHistory = (params) => { + const { page = 1, page_size = 10 } = params; + return paginate(mockChatHistory, page_size, page); +}; + +const mockCreateChat = (data) => { + const newChat = { + id: `chat-${uuidv4().slice(0, 8)}`, + title: data.title || '新对话', + knowledge_base_id: data.knowledge_base_id, + knowledge_base_name: data.knowledge_base_name, + message_count: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + mockChatHistory.unshift(newChat); + return newChat; +}; + +const mockUpdateChat = (id, data) => { + const index = mockChatHistory.findIndex((chat) => chat.id === id); + if (index === -1) { + throw new Error('Chat not found'); + } + const updatedChat = { + ...mockChatHistory[index], + ...data, + updated_at: new Date().toISOString(), + }; + mockChatHistory[index] = updatedChat; + return updatedChat; +}; + +const mockDeleteChat = (id) => { + const index = mockChatHistory.findIndex((chat) => chat.id === id); + if (index === -1) { + throw new Error('Chat not found'); + } + mockChatHistory.splice(index, 1); + return { success: true }; +}; // 模拟聊天消息数据 const chatMessages = {}; +// 模拟待处理权限申请 +const mockPendingRequests = [ + { + id: 1, + applicant: { + name: '王五', + department: '达人组', + }, + knowledge_base: { + name: '达人直播数据报告', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: false, + }, + reason: '需要查看和编辑直播数据报告', + created_at: '2024-01-07T10:30:00Z', + expires_at: null, + }, + { + id: 2, + applicant: { + name: '赵六', + department: '直播组', + }, + knowledge_base: { + name: '人力资源政策文件', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要了解最新的人力资源政策', + created_at: '2024-01-06T14:20:00Z', + expires_at: '2025-01-06T14:20:00Z', + }, + { + id: 3, + applicant: { + name: '钱七', + department: '市场部', + }, + knowledge_base: { + name: '市场分析报告', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要了解市场趋势', + created_at: '2024-01-05T09:15:00Z', + expires_at: '2024-07-05T09:15:00Z', + }, + { + id: 4, + applicant: { + name: '孙八', + department: '技术部', + }, + knowledge_base: { + name: '技术架构文档', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: true, + }, + reason: '需要进行技术架构更新', + created_at: '2024-01-04T16:45:00Z', + expires_at: null, + }, + { + id: 5, + applicant: { + name: '周九', + department: '产品部', + }, + knowledge_base: { + name: '产品规划文档', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: false, + }, + reason: '需要参与产品规划讨论', + created_at: '2024-01-03T11:30:00Z', + expires_at: '2024-12-31T23:59:59Z', + }, + { + id: 6, + applicant: { + name: '吴十', + department: '设计部', + }, + knowledge_base: { + name: '设计规范文档', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要参考设计规范', + created_at: '2024-01-02T14:20:00Z', + expires_at: '2024-06-30T23:59:59Z', + }, + { + id: 7, + applicant: { + name: '郑十一', + department: '财务部', + }, + knowledge_base: { + name: '财务报表', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + reason: '需要查看财务数据', + created_at: '2024-01-01T09:00:00Z', + expires_at: null, + }, +]; + +// 模拟用户权限详情 +const mockUserPermissions = { + 'user-001': [ + { + knowledge_base: { + id: 'kb-001', + name: '达人直播数据报告', + department: '达人组', + }, + permission: { + can_read: true, + can_edit: true, + can_admin: false, + }, + last_access_time: '2024-03-10T14:30:00Z', + }, + { + knowledge_base: { + id: 'kb-002', + name: '人力资源政策文件', + department: '人力资源组', + }, + permission: { + can_read: true, + can_edit: false, + can_admin: false, + }, + last_access_time: '2024-03-08T09:15:00Z', + }, + { + knowledge_base: { + id: 'kb-003', + name: '市场分析报告', + department: '市场部', + }, + permission: { + can_read: true, + can_edit: false, + can_admin: false, + }, + last_access_time: null, + }, + ], + 'user-002': [ + { + knowledge_base: { + id: 'kb-001', + name: '达人直播数据报告', + department: '达人组', + }, + permission: { + can_read: true, + can_edit: false, + can_admin: false, + }, + last_access_time: '2024-03-05T10:20:00Z', + }, + { + knowledge_base: { + id: 'kb-004', + name: '产品规划文档', + department: '产品部', + }, + permission: { + can_read: true, + can_edit: true, + can_admin: true, + }, + last_access_time: '2024-03-15T11:20:00Z', + }, + ], + 'user-003': [ + { + knowledge_base: { + id: 'kb-003', + name: '市场分析报告', + department: '市场部', + }, + permission: { + can_read: true, + can_edit: true, + can_admin: false, + }, + last_access_time: '2024-03-12T15:40:00Z', + }, + { + knowledge_base: { + id: 'kb-005', + name: 'UI/UX设计指南', + department: '设计部', + }, + permission: { + can_read: true, + can_edit: false, + can_admin: false, + }, + last_access_time: '2024-03-01T09:10:00Z', + }, + ], +}; + // Mock API functions export const mockGet = async (url, config = {}) => { console.log(`[MOCK API] GET ${url}`, config); @@ -239,12 +533,34 @@ export const mockGet = async (url, config = {}) => { // Simulate network delay await new Promise((resolve) => setTimeout(resolve, 500)); - // Get knowledge bases - if (url === '/knowledge-bases/') { + // Get current user + if (url === '/users/me/') { return { data: { - items: knowledgeBases, - total: knowledgeBases.length, + code: 200, + message: 'success', + data: { + user: mockUsers[0], // 默认返回第一个用户 + }, + }, + }; + } + + // Get knowledge bases + if (url === '/knowledge-bases/') { + const params = config.params || { page: 1, page_size: 10 }; + const result = paginate(knowledgeBases, params.page_size, params.page); + + return { + data: { + code: 200, + message: '获取知识库列表成功', + data: { + total: result.total, + page: result.page, + page_size: result.page_size, + items: result.items, + }, }, }; } @@ -258,13 +574,28 @@ export const mockGet = async (url, config = {}) => { throw { response: { status: 404, data: { message: 'Knowledge base not found' } } }; } - return { data: knowledgeBase }; + return { + data: { + code: 200, + message: 'success', + data: { + knowledge_base: knowledgeBase, + }, + }, + }; } // Get chat history if (url === '/chat-history/') { const params = config.params || { page: 1, page_size: 10 }; - return { data: mockGetChatHistory(params) }; + const result = mockGetChatHistory(params); + return { + data: { + code: 200, + message: 'success', + data: result, + }, + }; } // Get chat messages @@ -309,7 +640,55 @@ export const mockGet = async (url, config = {}) => { kb.description.toLowerCase().includes(keyword.toLowerCase()) || kb.tags.some((tag) => tag.toLowerCase().includes(keyword.toLowerCase())) ); - return paginate(filtered, page_size, page); + const result = paginate(filtered, page_size, page); + return { + data: { + code: 200, + message: 'success', + data: result, + }, + }; + } + + // 用户权限管理 - 获取用户列表 + if (url === '/users/permissions/') { + return { + data: { + code: 200, + message: 'success', + data: { + users: mockUsers, + }, + }, + }; + } + + // 用户权限管理 - 获取待处理申请 + if (url === '/permissions/pending/') { + return { + data: { + code: 200, + message: 'success', + data: { + pending_requests: mockPendingRequests, + }, + }, + }; + } + + // 用户权限管理 - 获取用户权限详情 + if (url.match(/\/users\/(.+)\/permissions\//)) { + const userId = url.match(/\/users\/(.+)\/permissions\//)[1]; + + return { + data: { + code: 200, + message: 'success', + data: { + permissions: mockUserPermissions[userId] || [], + }, + }, + }; } throw { response: { status: 404, data: { message: 'Not found' } } }; @@ -321,6 +700,47 @@ export const mockPost = async (url, data) => { // Simulate network delay await new Promise((resolve) => setTimeout(resolve, 800)); + // Login + if (url === '/auth/login/') { + const { username, password } = data; + const user = mockUsers.find((u) => u.username === username && u.password === password); + + if (!user) { + throw { + response: { + status: 401, + data: { + code: 401, + message: '用户名或密码错误', + }, + }, + }; + } + + // 在实际应用中,这里应该生成 JWT token + const token = `mock-jwt-token-${uuidv4()}`; + + return { + data: { + code: 200, + message: '登录成功', + data: { + token, + user: { + id: user.id, + username: user.username, + email: user.email, + name: user.name, + department: user.department, + group: user.group, + role: user.role, + avatar: user.avatar, + }, + }, + }, + }; + } + // Create knowledge base if (url === '/knowledge-bases/') { const newKnowledgeBase = { @@ -351,20 +771,30 @@ export const mockPost = async (url, data) => { knowledgeBases.push(newKnowledgeBase); - // 模拟后端返回格式 return { - code: 200, - message: '知识库创建成功', data: { - knowledge_base: newKnowledgeBase, - external_id: uuidv4(), + code: 200, + message: '知识库创建成功', + data: { + knowledge_base: newKnowledgeBase, + external_id: uuidv4(), + }, }, }; } // Create new chat if (url === '/chat-history/') { - return { data: mockCreateChat(data) }; + const newChat = mockCreateChat(data); + return { + data: { + code: 200, + message: 'success', + data: { + chat: newChat, + }, + }, + }; } // Send chat message @@ -421,6 +851,42 @@ export const mockPost = async (url, data) => { }; } + // 批准权限申请 + if (url === '/permissions/approve/') { + const { id, responseMessage } = data; + + // 从待处理列表中移除该申请 + mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id); + + return { + code: 200, + message: 'Permission approved successfully', + data: { + id: id, + status: 'approved', + response_message: responseMessage, + }, + }; + } + + // 拒绝权限申请 + if (url === '/permissions/reject/') { + const { id, responseMessage } = data; + + // 从待处理列表中移除该申请 + mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id); + + return { + code: 200, + message: 'Permission rejected successfully', + data: { + id: id, + status: 'rejected', + response_message: responseMessage, + }, + }; + } + throw { response: { status: 404, data: { message: 'Not found' } } }; }; @@ -465,10 +931,47 @@ export const mockPut = async (url, data) => { return { data: mockUpdateChat(id, data) }; } + // 更新用户权限 + if (url.match(/\/users\/(.+)\/permissions\//)) { + const userId = url.match(/\/users\/(.+)\/permissions\//)[1]; + const { permissions } = data; + + // 将权限更新应用到模拟数据 + if (mockUserPermissions[userId]) { + // 遍历permissions对象,更新对应知识库的权限 + Object.entries(permissions).forEach(([knowledgeBaseId, permissionType]) => { + // 查找该用户的该知识库权限 + const permissionIndex = mockUserPermissions[userId].findIndex( + (p) => p.knowledge_base.id === knowledgeBaseId + ); + + if (permissionIndex !== -1) { + // 根据权限类型设置具体权限 + const permission = { + can_read: permissionType === 'read' || permissionType === 'edit' || permissionType === 'admin', + can_edit: permissionType === 'edit' || permissionType === 'admin', + can_admin: permissionType === 'admin', + }; + + // 更新权限 + mockUserPermissions[userId][permissionIndex].permission = permission; + } + }); + } + + return { + code: 200, + message: 'Permissions updated successfully', + data: { + permissions: permissions, + }, + }; + } + throw { response: { status: 404, data: { message: 'Not found' } } }; }; -export const mockDel = async (url) => { +export const mockDelete = async (url) => { console.log(`[MOCK API] DELETE ${url}`); // Simulate network delay diff --git a/src/store/knowledgeBase/knowledgeBase.slice.js b/src/store/knowledgeBase/knowledgeBase.slice.js index d37445a..9b7ed7c 100644 --- a/src/store/knowledgeBase/knowledgeBase.slice.js +++ b/src/store/knowledgeBase/knowledgeBase.slice.js @@ -80,10 +80,7 @@ const knowledgeBaseSlice = createSlice({ }) .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.data = action.payload; state.list.error = null; }) .addCase(fetchKnowledgeBases.rejected, (state, action) => { diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js index c48f17a..757ba04 100644 --- a/src/store/knowledgeBase/knowledgeBase.thunks.js +++ b/src/store/knowledgeBase/knowledgeBase.thunks.js @@ -11,7 +11,11 @@ export const fetchKnowledgeBases = createAsyncThunk( 'knowledgeBase/fetchKnowledgeBases', async ({ page = 1, page_size = 10 } = {}, { rejectWithValue }) => { try { - const response = await get('/knowledge-bases/', { page, page_size }); + const response = await get('/knowledge-bases/', { params: { page, page_size } }); + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data; + } return response.data; } catch (error) { return rejectWithValue(error.response?.data || 'Failed to fetch knowledge bases');