[dev]knowledgebase mock data & pending requests

This commit is contained in:
susie-laptop 2025-03-19 13:06:52 -04:00
parent 6cf31165f9
commit b4a0874a4d
16 changed files with 2221 additions and 301 deletions

View File

@ -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 <AppRouter></AppRouter>;

View File

@ -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 (
<div className='api-mode-switch'>
<div className='d-flex align-items-center'>
<div className='form-check form-switch me-2'>
<input
className='form-check-input'
type='checkbox'
id='apiModeToggle'
checked={isMockMode}
onChange={handleToggleMode}
disabled={isChecking}
/>
<label className='form-check-label' htmlFor='apiModeToggle'>
{isChecking ? '检查中...' : isMockMode ? '模拟API模式' : '真实API模式'}
</label>
</div>
{isMockMode && <span className='badge bg-warning text-dark'>使用本地模拟数据</span>}
{!isMockMode && <span className='badge bg-success'>已连接到后端服务器</span>}
</div>
{showNotification && (
<div className={`alert alert-${notification.type} mt-2 py-2 px-3`} style={{ fontSize: '0.85rem' }}>
{notification.message}
</div>
)}
</div>
);
}

View File

@ -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 {

View File

@ -10,7 +10,7 @@ import { PersistGate } from 'redux-persist/integration/react';
import Loading from './components/Loading.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
// <StrictMode>
<PersistGate loading={<Loading />} persistor={persistor}>
<BrowserRouter>
<Provider store={store}>
@ -18,5 +18,5 @@ createRoot(document.getElementById('root')).render(
</Provider>
</BrowserRouter>
</PersistGate>
</StrictMode>
// </StrictMode>
);

View File

@ -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;

View File

@ -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;
}

View File

@ -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 (
<div className='permissions-container container py-4'>
<div className='permissions-container'>
<div className='api-mode-control mb-3'>
<ApiModeSwitch />
</div>
<ul className='nav nav-tabs mb-4'>
<li className='nav-item'>
<button
className={`nav-link ${activeTab === 'pending' ? 'active' : ''}`}
onClick={() => setActiveTab('pending')}
>
待处理申请
</button>
</li>
<li className='nav-item'>
<button
className={`nav-link ${activeTab === 'users' ? 'active' : ''}`}
onClick={() => setActiveTab('users')}
>
用户权限管理
</button>
</li>
</ul>
<div className='permissions-section mb-4'>
<PendingRequests />
</div>
<div className='tab-content'>{activeTab === 'pending' ? <PendingRequests /> : <UserPermissions />}</div>
<div className='permissions-section'>
<UserPermissions />
</div>
</div>
);
}

View File

@ -35,3 +35,217 @@
.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;
}

View File

@ -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 (
<div className='pagination-container'>
<button
className='pagination-button'
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
上一页
</button>
<div className='pagination-info'>
{currentPage} / {totalPages}
</div>
<button
className='pagination-button'
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
下一页
</button>
</div>
);
};
//
if (fetchStatus === 'loading') {
if (fetchStatus === 'loading' && pendingRequests.length === 0) {
return (
<div className='text-center py-5'>
<div className='spinner-border' role='status'>
@ -114,7 +368,7 @@ export default function PendingRequests() {
}
//
if (fetchStatus === 'failed') {
if (fetchStatus === 'failed' && pendingRequests.length === 0) {
return (
<div className='alert alert-danger' role='alert'>
{fetchError || '获取待处理申请失败'}
@ -131,95 +385,196 @@ export default function PendingRequests() {
);
}
//
const currentPageData = getCurrentPageData();
//
return (
<>
<div className='d-flex justify-content-between align-items-center mb-4'>
{/* <h5 className='mb-0'>待处理申请</h5> */}
<div className='bg-danger rounded-pill text-white py-1 px-2'>{pendingRequests.length}个待处理</div>
<div className='d-flex justify-content-between align-items-center mb-3'>
<h5 className='mb-0'>待处理申请</h5>
<div className='badge bg-danger'>{pendingRequests.length}个待处理</div>
</div>
<div className='permission-requests'>
{pendingRequests.map((request) => (
<div key={request.id} className='permission-card card mb-3 border-0 shadow-sm bg-light text-dark'>
<div className='card-body p-4 d-flex flex-column gap-3'>
<div className='d-flex justify-content-between'>
<div>
<h5 className='mb-1'>{request.applicant.name || request.applicant}</h5>
<p className='text-dark-50 mb-0'>{request.applicant.department || '无归属部门'}</p>
</div>
<div className='text-end'>
<div className='text-dark-50 mb-0'>
{new Date(request.created_at).toLocaleDateString()}
<div className='pending-requests-list'>
{currentPageData.map((request) => (
<div key={request.id} className='pending-request-item' onClick={() => handleRowClick(request)}>
<div className='request-header'>
<div className='user-info'>
<h6 className='mb-0'>{request.applicant.name}</h6>
<p className='department'>{request.applicant.department}</p>
</div>
<div className='request-date'>{new Date(request.created_at).toLocaleDateString()}</div>
</div>
<div className='request-content'>
<p className='mb-2'>申请访问{request.knowledge_base.name}</p>
{request.permissions.can_edit ? (
<span
className='badge bg-success-subtle text-success d-flex align-items-center gap-1'
style={{ width: 'fit-content' }}
>
<SvgIcon className={'circle-yes'} />
完全访问
</span>
) : (
request.permissions.can_read && (
<span
className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'
style={{ width: 'fit-content' }}
>
<SvgIcon className={'eye'} />
只读访问
</span>
)
)}
</div>
<div className='request-actions'>
<button
className='btn btn-outline-danger'
onClick={(e) => {
e.stopPropagation();
handleDirectProcess(request.id, false);
}}
disabled={processingId === request.id && approveRejectStatus === 'loading'}
>
{processingId === request.id && approveRejectStatus === 'loading' && !isApproving
? '处理中...'
: '拒绝'}
</button>
<button
className='btn btn-success'
onClick={(e) => {
e.stopPropagation();
handleDirectProcess(request.id, true);
}}
disabled={processingId === request.id && approveRejectStatus === 'loading'}
>
{processingId === request.id && approveRejectStatus === 'loading' && isApproving
? '处理中...'
: '批准'}
</button>
</div>
</div>
))}
</div>
{/* 分页控件 */}
{renderPagination()}
{/* 滑动面板 */}
<div className={`slide-over-backdrop ${showSlideOver ? 'show' : ''}`} onClick={handleCloseSlideOver}></div>
<div className={`slide-over ${showSlideOver ? 'show' : ''}`}>
{selectedRequest && (
<div className='slide-over-content'>
<div className='slide-over-header'>
<h5 className='mb-0'>申请详情</h5>
<button type='button' className='btn-close' onClick={handleCloseSlideOver}></button>
</div>
<div className='slide-over-body'>
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请人信息</h6>
<div className='d-flex align-items-center mb-3'>
<div className='avatar-placeholder me-3 bg-dark'>
{(selectedRequest.applicant.name || selectedRequest.applicant).charAt(0)}
</div>
<div>
<h5 className='mb-1'>
{selectedRequest.applicant.name || selectedRequest.applicant}
</h5>
<p className='text-muted mb-0'>
{selectedRequest.applicant.department || '无归属部门'}
</p>
</div>
</div>
</div>
<div className='d-flex flex-column gap-3'>
<div>申请访问{request.knowledge_base.name || request.knowledge_base}</div>
<div className='d-flex flex-wrap gap-2'>
{request.permissions.can_read && (
<span className='badge bg-info custom-badge'>只读</span>
<div className='mb-4'>
<h6 className='text-muted mb-2'>知识库信息</h6>
<p className='mb-1'>
<strong>名称</strong>{' '}
{selectedRequest.knowledge_base.name || selectedRequest.knowledge_base}
</p>
</div>
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请权限</h6>
<div className='d-flex flex-wrap gap-2 mb-3'>
{selectedRequest.permissions.can_read && !selectedRequest.permissions.can_edit && (
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
只读
</span>
)}
{request.permissions.can_edit && (
<span className='badge bg-success custom-badge'>编辑</span>
)}
{request.permissions.can_delete && (
<span className='badge bg-danger custom-badge'>删除</span>
)}
{request.expires_at && (
<span className='badge bg-secondary custom-badge'>
{new Date(request.expires_at).toLocaleDateString()}
{selectedRequest.permissions.can_edit && selectedRequest.permissions.can_delete && (
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
完全访问
</span>
)}
</div>
<div className='text-dark-50'>
<strong>申请理由</strong> {request.reason}
</div>
</div>
<div className='d-flex justify-content-end'>
<button
className='btn btn-outline-danger me-2'
onClick={() => handleOpenResponseInput(request.id, false)}
disabled={processingId === request.id && approveRejectStatus === 'loading'}
>
{processingId === request.id &&
approveRejectStatus === 'loading' &&
!isApproving ? (
<>
<span
className='spinner-border spinner-border-sm me-1'
role='status'
aria-hidden='true'
></span>
处理中...
</>
) : (
<>拒绝</>
)}
</button>
<button
className='btn btn-success'
onClick={() => handleOpenResponseInput(request.id, true)}
disabled={processingId === request.id && approveRejectStatus === 'loading'}
>
{processingId === request.id && approveRejectStatus === 'loading' && isApproving ? (
<>
<span
className='spinner-border spinner-border-sm me-1'
role='status'
aria-hidden='true'
></span>
处理中...
</>
) : (
<>批准</>
)}
</button>
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请时间</h6>
<p className='mb-1'>{new Date(selectedRequest.created_at).toLocaleString()}</p>
</div>
{selectedRequest.expires_at && (
<div className='mb-4'>
<h6 className='text-muted mb-2'>到期时间</h6>
<p className='mb-1'>{new Date(selectedRequest.expires_at).toLocaleString()}</p>
</div>
)}
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请理由</h6>
<div className='p-3 bg-light rounded'>{selectedRequest.reason || '无申请理由'}</div>
</div>
</div>
<div className='slide-over-footer'>
<button
className='btn btn-outline-danger me-2'
onClick={() => handleOpenResponseInput(selectedRequest.id, false)}
disabled={processingId === selectedRequest.id && approveRejectStatus === 'loading'}
>
{processingId === selectedRequest.id &&
approveRejectStatus === 'loading' &&
!isApproving ? (
<>
<span
className='spinner-border spinner-border-sm me-1'
role='status'
aria-hidden='true'
></span>
处理中...
</>
) : (
<>拒绝</>
)}
</button>
<button
className='btn btn-success'
onClick={() => handleOpenResponseInput(selectedRequest.id, true)}
disabled={processingId === selectedRequest.id && approveRejectStatus === 'loading'}
>
{processingId === selectedRequest.id &&
approveRejectStatus === 'loading' &&
isApproving ? (
<>
<span
className='spinner-border spinner-border-sm me-1'
role='status'
aria-hidden='true'
></span>
处理中...
</>
) : (
<>批准</>
)}
</button>
</div>
</div>
))}
)}
</div>
{/* 回复输入弹窗 */}

View File

@ -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 (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog modal-lg modal-dialog-scrollable'>
<div className='modal-dialog modal-lg modal-dialog-scrollable' style={{ zIndex: 9999 }}>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title'>{user.name} 的权限详情</h5>
@ -87,8 +159,76 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
<p className='mt-3'>加载权限详情...</p>
</div>
) : error ? (
<div className='alert alert-danger' role='alert'>
{error}
<div>
<div className='alert alert-warning' role='alert'>
{error}
</div>
{userPermissions.length > 0 && (
<div className='table-responsive'>
<table className='table table-hover'>
<thead className='table-light'>
<tr>
<th>知识库名称</th>
<th>所属部门</th>
<th>当前权限</th>
<th>最后访问时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{userPermissions.map((item) => {
const currentPermissionType = getPermissionType(item.permission);
const updatedPermissionType =
updatedPermissions[item.knowledge_base.id] || currentPermissionType;
return (
<tr key={item.knowledge_base.id}>
<td>{item.knowledge_base.name}</td>
<td>{item.knowledge_base.department || '未指定'}</td>
<td>
<span
className={`badge ${
currentPermissionType === 'admin'
? 'bg-danger'
: currentPermissionType === 'edit'
? 'bg-success'
: currentPermissionType === 'read'
? 'bg-info'
: 'bg-secondary'
}`}
>
{getPermissionTypeText(currentPermissionType)}
</span>
</td>
<td>
{item.last_access_time
? new Date(item.last_access_time).toLocaleString()
: '从未访问'}
</td>
<td>
<select
className='form-select form-select-sm'
value={updatedPermissionType}
onChange={(e) =>
handlePermissionChange(
item.knowledge_base.id,
e.target.value
)
}
>
<option value='none'>无权限</option>
<option value='read'>只读访问</option>
<option value='edit'>编辑权限</option>
<option value='admin'>管理权限</option>
</select>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
) : userPermissions.length === 0 ? (
<div className='alert alert-info' role='alert'>
@ -167,7 +307,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</button>
<button
type='button'
className='btn btn-primary'
className='btn btn-dark'
onClick={handleSave}
disabled={loading || Object.keys(updatedPermissions).length === 0}
>
@ -176,7 +316,7 @@ export default function UserPermissionDetails({ user, onClose, onSave }) {
</div>
</div>
</div>
<div className='modal-backdrop fade show'></div>
<div className='modal-backdrop fade show' style={{ zIndex: 1050 }}></div>
</div>
);
}

View File

@ -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;
}

View File

@ -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 (
<div className='d-flex justify-content-center align-items-center mt-4'>
<nav aria-label='用户权限分页'>
<ul className='pagination'>
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
<button
className='page-link'
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
上一页
</button>
</li>
<li className='page-item active'>
<span className='page-link'>
{currentPage} / {totalPages}
</span>
</li>
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
<button
className='page-link'
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
下一页
</button>
</li>
</ul>
</nav>
<div className='ms-3'>
<select
className='form-select form-select-sm'
value={pageSize}
onChange={handlePageSizeChange}
aria-label='每页显示数量'
>
{PAGE_SIZE_OPTIONS.map((size) => (
<option key={size} value={size}>
{size}/
</option>
))}
</select>
</div>
</div>
);
};
//
if (loading) {
if (loading && users.length === 0) {
return (
<div className='text-center py-5'>
<div className='spinner-border' role='status'>
@ -112,7 +336,7 @@ export default function UserPermissions() {
}
//
if (error) {
if (error && users.length === 0) {
return (
<div className='alert alert-danger' role='alert'>
{error}
@ -129,70 +353,111 @@ export default function UserPermissions() {
);
}
//
const currentPageData = getCurrentPageData();
//
const filteredUsersCount = getFilteredUsers().length;
//
return (
<>
<div className='card'>
<div className='card-header bg-white'>
<h5 className='mb-0'>用户权限管理</h5>
</div>
<div className='card-body p-0'>
<div className='table-responsive'>
<table className='table table-hover mb-0'>
<thead className='table-light'>
<tr>
<th>用户名</th>
<th>姓名</th>
<th>部门</th>
<th>职位</th>
<th>权限</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.username}</td>
<td>{user.name}</td>
<td>{user.department}</td>
<td>{user.position}</td>
<td>
{user.permissions_count && (
<div className='d-flex gap-2'>
{user.permissions_count.read > 0 && (
<span className='badge bg-info'>
只读: {user.permissions_count.read}
</span>
)}
{user.permissions_count.edit > 0 && (
<span className='badge bg-success'>
编辑: {user.permissions_count.edit}
</span>
)}
{user.permissions_count.admin > 0 && (
<span className='badge bg-danger'>
管理: {user.permissions_count.admin}
</span>
)}
</div>
)}
</td>
<td>
<button
className='btn btn-sm btn-primary'
onClick={() => handleOpenDetailsModal(user)}
>
权限详情
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className='d-flex justify-content-between align-items-center mb-3'>
<h5 className='mb-0'>用户权限管理</h5>
<div className='input-group' style={{ maxWidth: '250px' }}>
<input
type='text'
className='form-control'
placeholder='搜索用户...'
value={searchTerm}
onChange={handleSearchChange}
/>
<span className='input-group-text'>
<i className='bi bi-search'></i>
</span>
</div>
</div>
{filteredUsersCount > 0 ? (
<>
<div className='card'>
<div className='table-responsive'>
<table className='table table-hover mb-0'>
<thead className='table-light'>
<tr>
<th scope='col'>用户</th>
<th scope='col'>部门</th>
<th scope='col'>职位</th>
<th scope='col'>数据集权限</th>
<th scope='col' className='text-end'>
操作
</th>
</tr>
</thead>
<tbody>
{currentPageData.map((user) => (
<tr key={user.id}>
<td>
<div className='d-flex align-items-center'>
<div
className='bg-dark rounded-circle text-white d-flex align-items-center justify-content-center me-2'
style={{ width: '36px', height: '36px' }}
>
{user.name.charAt(0)}
</div>
<div>
<div className='fw-medium'>{user.name}</div>
<div className='text-muted small'>{user.username}</div>
</div>
</div>
</td>
<td className='align-middle'>{user.department}</td>
<td className='align-middle'>{user.position}</td>
<td className='align-middle'>
{user.permissions_count && (
<div className='d-flex flex-wrap gap-1'>
{user.permissions_count.read > 0 && (
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
完全访问: {user.permissions_count.read}
</span>
)}
{user.permissions_count.edit > 0 && (
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
只读访问: {user.permissions_count.edit}
</span>
)}
{user.permissions_count.admin > 0 && (
<span className='badge bg-danger d-flex align-items-center gap-1'>
无访问权限: {user.permissions_count.admin}
</span>
)}
</div>
)}
</td>
<td className='text-end align-middle'>
<button
className='btn btn-outline-dark btn-sm'
onClick={() => handleOpenDetailsModal(user)}
>
修改权限
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 分页控件 */}
{renderPagination()}
</>
) : (
<div className='alert alert-info' role='alert'>
没有找到匹配的用户
</div>
)}
{/* 用户权限详情弹窗 */}
{showDetailsModal && selectedUser && (
<UserPermissionDetails

View File

@ -1,8 +1,13 @@
import axios from 'axios';
import CryptoJS from 'crypto-js';
import { mockGet, mockPost, mockPut, mockDelete } from './mockApi';
const secretKey = import.meta.env.VITE_SECRETKEY;
// API连接状态
let isServerDown = false;
let hasCheckedServer = false;
// Create Axios instance with base URL
const api = axios.create({
baseURL: '/api',
@ -28,9 +33,22 @@ api.interceptors.request.use(
// Response Interceptor
api.interceptors.response.use(
(response) => {
// 如果成功收到响应,表示服务器正常工作
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 };

View File

@ -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

View File

@ -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) => {

View File

@ -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');