[dev]add permission page

This commit is contained in:
susie-laptop 2025-03-12 22:24:05 -04:00
parent c9236cfff4
commit 6cf31165f9
17 changed files with 982 additions and 387 deletions

View File

@ -8,6 +8,7 @@ export default function HeaderWithNav() {
const navigate = useNavigate();
const location = useLocation();
const { user } = useSelector((state) => state.auth);
console.log('user', user);
const handleLogout = async () => {
try {
@ -22,6 +23,9 @@ export default function HeaderWithNav() {
return location.pathname.startsWith(path);
};
// leader admin
const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
return (
<header className=' navbar navbar-expand-lg p-0'>
<nav className='navbar navbar-expand-lg border-bottom p-3 w-100'>
@ -44,7 +48,9 @@ export default function HeaderWithNav() {
<ul className='navbar-nav me-auto mb-lg-0'>
<li className='nav-item'>
<Link
className={`nav-link ${isActive('/') && !isActive('/chat') ? 'active' : ''}`}
className={`nav-link ${
isActive('/') && !isActive('/chat') && !isActive('/permissions') ? 'active' : ''
}`}
aria-current='page'
to='/'
>
@ -56,6 +62,16 @@ export default function HeaderWithNav() {
Chat
</Link>
</li>
{hasManagePermission && (
<li className='nav-item'>
<Link
className={`nav-link ${isActive('/permissions') ? 'active' : ''}`}
to='/permissions'
>
权限管理
</Link>
</li>
)}
</ul>
{!!user ? (
<div className='flex-shrink-0 dropdown'>

View File

@ -0,0 +1,44 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import PendingRequests from './components/PendingRequests';
import UserPermissions from './components/UserPermissions';
export default function PermissionsPage() {
const navigate = useNavigate();
const { user } = useSelector((state) => state.auth);
const [activeTab, setActiveTab] = useState('pending');
// leader admin
useEffect(() => {
if (!user || (user.role !== 'leader' && user.role !== 'admin')) {
navigate('/');
}
}, [user, navigate]);
return (
<div className='permissions-container container py-4'>
<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='tab-content'>{activeTab === 'pending' ? <PendingRequests /> : <UserPermissions />}</div>
</div>
);
}

View File

@ -0,0 +1,37 @@
.permission-requests {
display: flex;
flex-direction: column;
gap: 1rem;
}
.permission-card {
transition: all 0.2s ease;
border-radius: 8px;
background-color: #343a40;
}
.permission-card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.custom-badge {
padding: 0.35em 0.65em;
font-size: 0.85em;
}
.badge.bg-info {
background-color: #0dcaf0 !important;
}
.badge.bg-success {
background-color: #198754 !important;
}
.badge.bg-danger {
background-color: #dc3545 !important;
}
.badge.bg-secondary {
background-color: #6c757d !important;
}

View File

@ -0,0 +1,292 @@
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { showNotification } from '../../../store/notification.slice';
import {
fetchPermissionsThunk,
approvePermissionThunk,
rejectPermissionThunk,
} from '../../../store/permissions/permissions.thunks';
import { resetApproveRejectStatus } from '../../../store/permissions/permissions.slice';
import './PendingRequests.css'; // CSS
export default function PendingRequests() {
const dispatch = useDispatch();
const [responseMessage, setResponseMessage] = useState('');
const [showResponseInput, setShowResponseInput] = useState(false);
const [currentRequestId, setCurrentRequestId] = useState(null);
const [isApproving, setIsApproving] = useState(false);
// Redux store
const {
items: pendingRequests,
status: fetchStatus,
error: fetchError,
} = useSelector((state) => state.permissions.permissions);
const {
status: approveRejectStatus,
error: approveRejectError,
currentId: processingId,
} = useSelector((state) => state.permissions.approveReject);
//
useEffect(() => {
dispatch(fetchPermissionsThunk());
}, [dispatch]);
// /
useEffect(() => {
if (approveRejectStatus === 'succeeded') {
dispatch(
showNotification({
message: isApproving ? '已批准申请' : '已拒绝申请',
type: 'success',
})
);
setShowResponseInput(false);
setCurrentRequestId(null);
setResponseMessage('');
//
dispatch(resetApproveRejectStatus());
} else if (approveRejectStatus === 'failed') {
dispatch(
showNotification({
message: approveRejectError || '处理申请失败',
type: 'danger',
})
);
//
dispatch(resetApproveRejectStatus());
}
}, [approveRejectStatus, approveRejectError, dispatch, isApproving]);
//
const handleOpenResponseInput = (requestId, approving) => {
setCurrentRequestId(requestId);
setIsApproving(approving);
setShowResponseInput(true);
};
//
const handleCloseResponseInput = () => {
setShowResponseInput(false);
setCurrentRequestId(null);
setResponseMessage('');
};
//
const handleProcessRequest = () => {
if (!currentRequestId) return;
const params = {
id: currentRequestId,
responseMessage,
};
if (isApproving) {
dispatch(approvePermissionThunk(params));
} else {
dispatch(rejectPermissionThunk(params));
}
};
//
const getPermissionTypeText = (permissions) => {
if (permissions.can_read && !permissions.can_edit) {
return '只读访问';
} else if (permissions.can_edit) {
return '完全访问';
} else {
return '未知权限';
}
};
//
if (fetchStatus === 'loading') {
return (
<div className='text-center py-5'>
<div className='spinner-border' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
<p className='mt-3'>加载待处理申请...</p>
</div>
);
}
//
if (fetchStatus === 'failed') {
return (
<div className='alert alert-danger' role='alert'>
{fetchError || '获取待处理申请失败'}
</div>
);
}
//
if (pendingRequests.length === 0) {
return (
<div className='alert alert-info' role='alert'>
暂无待处理的权限申请
</div>
);
}
//
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>
<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>
</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>
)}
{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()}
</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>
</div>
</div>
))}
</div>
{/* 回复输入弹窗 */}
{showResponseInput && (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title'>{isApproving ? '批准' : '拒绝'}申请</h5>
<button
type='button'
className='btn-close'
onClick={handleCloseResponseInput}
disabled={approveRejectStatus === 'loading'}
></button>
</div>
<div className='modal-body'>
<div className='mb-3'>
<label htmlFor='responseMessage' className='form-label'>
审批意见
</label>
<textarea
className='form-control'
id='responseMessage'
rows='3'
value={responseMessage}
onChange={(e) => setResponseMessage(e.target.value)}
placeholder={isApproving ? '请输入批准意见(可选)' : '请输入拒绝理由(可选)'}
></textarea>
</div>
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-secondary'
onClick={handleCloseResponseInput}
disabled={approveRejectStatus === 'loading'}
>
取消
</button>
<button
type='button'
className={`btn ${isApproving ? 'btn-success' : 'btn-danger'}`}
onClick={handleProcessRequest}
disabled={approveRejectStatus === 'loading'}
>
{approveRejectStatus === 'loading' ? (
<>
<span
className='spinner-border spinner-border-sm me-1'
role='status'
aria-hidden='true'
></span>
处理中...
</>
) : isApproving ? (
'确认批准'
) : (
'确认拒绝'
)}
</button>
</div>
</div>
</div>
<div className='modal-backdrop fade show'></div>
</div>
)}
</>
);
}

View File

@ -0,0 +1,182 @@
import React, { useState, useEffect } from 'react';
import { get } from '../../../services/api';
export default function UserPermissionDetails({ user, onClose, onSave }) {
const [userPermissions, setUserPermissions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [updatedPermissions, setUpdatedPermissions] = useState({});
//
useEffect(() => {
const fetchUserPermissions = async () => {
try {
setLoading(true);
const response = await get(`/users/${user.id}/permissions/`);
if (response && response.code === 200) {
setUserPermissions(response.data.permissions || []);
} else {
setError('获取用户权限详情失败');
}
} catch (error) {
console.error('获取用户权限详情失败:', error);
setError('获取用户权限详情失败');
} finally {
setLoading(false);
}
};
if (user) {
fetchUserPermissions();
}
}, [user]);
//
const handlePermissionChange = (knowledgeBaseId, permissionType) => {
setUpdatedPermissions({
...updatedPermissions,
[knowledgeBaseId]: permissionType,
});
};
//
const handleSave = () => {
onSave(user.id, updatedPermissions);
};
//
const getPermissionTypeText = (permissionType) => {
switch (permissionType) {
case 'none':
return '无权限';
case 'read':
return '只读访问';
case 'edit':
return '编辑权限';
case 'admin':
return '管理权限';
default:
return '未知权限';
}
};
//
const getPermissionType = (permission) => {
if (!permission) return 'none';
if (permission.can_admin) return 'admin';
if (permission.can_edit) return 'edit';
if (permission.can_read) return 'read';
return 'none';
};
return (
<div className='modal fade show' style={{ display: 'block' }} tabIndex='-1'>
<div className='modal-dialog modal-lg modal-dialog-scrollable'>
<div className='modal-content'>
<div className='modal-header'>
<h5 className='modal-title'>{user.name} 的权限详情</h5>
<button type='button' className='btn-close' onClick={onClose}></button>
</div>
<div className='modal-body'>
{loading ? (
<div className='text-center py-4'>
<div className='spinner-border' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
<p className='mt-3'>加载权限详情...</p>
</div>
) : error ? (
<div className='alert alert-danger' role='alert'>
{error}
</div>
) : userPermissions.length === 0 ? (
<div className='alert alert-info' role='alert'>
该用户暂无任何知识库权限
</div>
) : (
<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>
<div className='modal-footer'>
<button type='button' className='btn btn-secondary' onClick={onClose}>
关闭
</button>
<button
type='button'
className='btn btn-primary'
onClick={handleSave}
disabled={loading || Object.keys(updatedPermissions).length === 0}
>
保存更改
</button>
</div>
</div>
</div>
<div className='modal-backdrop fade show'></div>
</div>
);
}

View File

@ -0,0 +1,206 @@
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { get, put } from '../../../services/api';
import { showNotification } from '../../../store/notification.slice';
import UserPermissionDetails from './UserPermissionDetails';
export default function UserPermissions() {
const dispatch = useDispatch();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedUser, setSelectedUser] = useState(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
//
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await get('/users/permissions/');
if (response && response.code === 200) {
setUsers(response.data.users || []);
} else {
setError('获取用户列表失败');
}
} catch (error) {
console.error('获取用户列表失败:', error);
setError('获取用户列表失败');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
//
const handleOpenDetailsModal = (user) => {
setSelectedUser(user);
setShowDetailsModal(true);
};
//
const handleCloseDetailsModal = () => {
setShowDetailsModal(false);
setSelectedUser(null);
};
//
const handleSavePermissions = async (userId, updatedPermissions) => {
try {
const response = await put(`/users/${userId}/permissions/`, {
permissions: updatedPermissions,
});
if (response && response.code === 200) {
//
setUsers(
users.map((user) => {
if (user.id === userId) {
return {
...user,
permissions: {
...user.permissions,
...response.data.permissions,
},
};
}
return user;
})
);
dispatch(
showNotification({
message: '权限更新成功',
type: 'success',
})
);
//
handleCloseDetailsModal();
} else {
dispatch(
showNotification({
message: '权限更新失败',
type: 'danger',
})
);
}
} catch (error) {
console.error('权限更新失败:', error);
dispatch(
showNotification({
message: '权限更新失败',
type: 'danger',
})
);
}
};
//
if (loading) {
return (
<div className='text-center py-5'>
<div className='spinner-border' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
<p className='mt-3'>加载用户列表...</p>
</div>
);
}
//
if (error) {
return (
<div className='alert alert-danger' role='alert'>
{error}
</div>
);
}
//
if (users.length === 0) {
return (
<div className='alert alert-info' role='alert'>
暂无用户数据
</div>
);
}
//
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>
</div>
{/* 用户权限详情弹窗 */}
{showDetailsModal && selectedUser && (
<UserPermissionDetails
user={selectedUser}
onClose={handleCloseDetailsModal}
onSave={handleSavePermissions}
/>
)}
</>
);
}

View File

@ -4,6 +4,7 @@ import Mainlayout from '../layouts/Mainlayout';
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
import KnowledgeBaseDetail from '../pages/KnowledgeBase/Detail/KnowledgeBaseDetail';
import Chat from '../pages/Chat/Chat';
import PermissionsPage from '../pages/Permissions/PermissionsPage';
import Loading from '../components/Loading';
import Login from '../pages/Auth/Login';
import Signup from '../pages/Auth/Signup';
@ -13,6 +14,9 @@ import { useSelector } from 'react-redux';
function AppRouter() {
const { user } = useSelector((state) => state.auth);
// leader admin
const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
return (
<Suspense fallback={<Loading />}>
<Routes>
@ -65,6 +69,15 @@ function AppRouter() {
</Mainlayout>
}
/>
{/* 权限管理页面路由 - 仅对 leader 或 admin 角色可见 */}
<Route
path='/permissions'
element={
<Mainlayout>
<PermissionsPage />
</Mainlayout>
}
/>
</Route>
<Route path='/login' element={<Login />} />
<Route path='/signup' element={<Signup />} />

View File

@ -0,0 +1,55 @@
import { post } from './api';
/**
* 计算权限过期时间
* @param {string} duration - 权限持续时间 '一周', '一个月', '三个月', '六个月', '永久'
* @returns {string} - ISO 格式的日期字符串
*/
export const calculateExpiresAt = (duration) => {
const now = new Date();
switch (duration) {
case '一周':
now.setDate(now.getDate() + 7);
break;
case '一个月':
now.setMonth(now.getMonth() + 1);
break;
case '三个月':
now.setMonth(now.getMonth() + 3);
break;
case '六个月':
now.setMonth(now.getMonth() + 6);
break;
case '永久':
// 设置为较远的未来日期
now.setFullYear(now.getFullYear() + 10);
break;
default:
now.setDate(now.getDate() + 7);
}
return now.toISOString();
};
/**
* 申请知识库访问权限
* @param {Object} requestData - 请求数据
* @param {string} requestData.id - 知识库ID
* @param {string} requestData.accessType - 访问类型 '只读访问', '编辑权限'
* @param {string} requestData.duration - 访问时长 '一周', '一个月'
* @param {string} requestData.reason - 申请原因
* @returns {Promise} - API 请求的 Promise
*/
export const requestKnowledgeBaseAccess = async (requestData) => {
const apiRequestData = {
knowledge_base: requestData.id,
permissions: {
can_read: true,
can_edit: requestData.accessType === '编辑权限',
can_delete: false,
},
reason: requestData.reason,
expires_at: calculateExpiresAt(requestData.duration),
};
return post('/permissions/', apiRequestData);
};

View File

@ -1,13 +0,0 @@
// 模拟的当前用户数据
export const mockCurrentUser = {
id: 'user-001',
username: 'johndoe',
email: 'john@example.com',
name: 'John Doe',
department: '研发部',
group: '前端开发组',
role: 'developer',
avatar: 'https://via.placeholder.com/150',
created_at: '2023-01-15T08:30:00Z',
updated_at: '2023-12-20T14:45:00Z',
};

View File

@ -1,6 +1,5 @@
import { createSlice } from '@reduxjs/toolkit';
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
import { mockCurrentUser } from './auth.mock';
const setPending = (state) => {
state.loading = true;
@ -24,7 +23,7 @@ const authSlice = createSlice({
initialState: {
loading: false,
error: null,
user: mockCurrentUser, // 使用模拟的当前用户数据
user: null,
},
reducers: {
login: (state, action) => {

View File

@ -54,7 +54,8 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje
export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
try {
const { user, message } = await post('/auth/verify-token/');
const { data, message } = await post('/auth/verify-token/');
const { user } = data;
if (!user) {
dispatch(logout());
throw new Error(message || 'No token found');

View File

@ -1,153 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
// 模拟聊天历史数据
export const mockChatHistory = [
{
id: '1',
title: '关于产品开发流程的咨询',
knowledge_base_id: '1',
knowledge_base_name: '产品开发知识库',
created_at: '2025-03-10T10:30:00Z',
updated_at: '2025-03-10T11:45:00Z',
message_count: 8,
},
{
id: '2',
title: '市场分析报告查询',
knowledge_base_id: '2',
knowledge_base_name: '市场分析知识库',
created_at: '2025-03-09T14:20:00Z',
updated_at: '2025-03-09T15:10:00Z',
message_count: 5,
},
{
id: '3',
title: '技术架构设计讨论',
knowledge_base_id: '4',
knowledge_base_name: '技术架构知识库',
created_at: '2025-03-08T09:15:00Z',
updated_at: '2025-03-08T10:30:00Z',
message_count: 12,
},
{
id: '4',
title: '用户反馈分析',
knowledge_base_id: '5',
knowledge_base_name: '用户研究知识库',
created_at: '2025-03-07T16:40:00Z',
updated_at: '2025-03-07T17:25:00Z',
message_count: 6,
},
];
// 内存存储,用于模拟数据库操作
let chatHistoryStore = [...mockChatHistory];
/**
* 模拟获取聊天历史列表
* @param {Object} params - 查询参数
* @returns {Object} - 分页结果
*/
export const mockGetChatHistory = (params = { page: 1, page_size: 10 }) => {
const { page, page_size } = params;
const startIndex = (page - 1) * page_size;
const endIndex = startIndex + page_size;
const paginatedItems = chatHistoryStore.slice(startIndex, endIndex);
return {
code: 200,
message: '获取成功',
data: {
total: chatHistoryStore.length,
page,
page_size,
results: paginatedItems,
},
};
};
/**
* 模拟创建新聊天
* @param {Object} chatData - 聊天数据
* @returns {Object} - 创建结果
*/
export const mockCreateChat = (chatData) => {
const newChat = {
id: uuidv4(),
title: chatData.title || '新的聊天',
knowledge_base_id: chatData.knowledge_base_id,
knowledge_base_name: chatData.knowledge_base_name || '未知知识库',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
message_count: 0,
};
chatHistoryStore.unshift(newChat);
return {
code: 200,
message: '创建成功',
data: {
chat: newChat,
},
};
};
/**
* 模拟更新聊天
* @param {string} id - 聊天ID
* @param {Object} data - 更新数据
* @returns {Object} - 更新结果
*/
export const mockUpdateChat = (id, data) => {
const index = chatHistoryStore.findIndex((chat) => chat.id === id);
if (index === -1) {
return {
code: 404,
message: '聊天不存在',
data: null,
};
}
const updatedChat = {
...chatHistoryStore[index],
...data,
updated_at: new Date().toISOString(),
};
chatHistoryStore[index] = updatedChat;
return {
code: 200,
message: '更新成功',
data: {
chat: updatedChat,
},
};
};
/**
* 模拟删除聊天
* @param {string} id - 聊天ID
* @returns {Object} - 删除结果
*/
export const mockDeleteChat = (id) => {
const index = chatHistoryStore.findIndex((chat) => chat.id === id);
if (index === -1) {
return {
code: 404,
message: '聊天不存在',
data: null,
};
}
chatHistoryStore.splice(index, 1);
return {
code: 200,
message: '删除成功',
data: null,
};
};

View File

@ -1,130 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
import { fetchChatHistory, createChat, deleteChat, updateChat } from './chatHistory.thunks';
// 初始状态
const initialState = {
// 聊天历史列表
list: {
items: [],
total: 0,
page: 1,
page_size: 10,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
// 当前聊天
currentChat: {
data: null,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
// 操作状态(创建、更新、删除)
operations: {
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
};
// 创建 slice
const chatHistorySlice = createSlice({
name: 'chatHistory',
initialState,
reducers: {
// 重置操作状态
resetOperationStatus: (state) => {
state.operations.status = 'idle';
state.operations.error = null;
},
// 重置当前聊天
resetCurrentChat: (state) => {
state.currentChat.data = null;
state.currentChat.status = 'idle';
state.currentChat.error = null;
},
// 设置当前聊天
setCurrentChat: (state, action) => {
state.currentChat.data = action.payload;
state.currentChat.status = 'succeeded';
},
},
extraReducers: (builder) => {
// 获取聊天历史
builder
.addCase(fetchChatHistory.pending, (state) => {
state.list.status = 'loading';
})
.addCase(fetchChatHistory.fulfilled, (state, action) => {
state.list.status = 'succeeded';
state.list.items = action.payload.results;
state.list.total = action.payload.total;
state.list.page = action.payload.page;
state.list.page_size = action.payload.page_size;
})
.addCase(fetchChatHistory.rejected, (state, action) => {
state.list.status = 'failed';
state.list.error = action.payload || action.error.message;
})
// 创建聊天
.addCase(createChat.pending, (state) => {
state.operations.status = 'loading';
})
.addCase(createChat.fulfilled, (state, action) => {
state.operations.status = 'succeeded';
state.list.items.unshift(action.payload);
state.list.total += 1;
state.currentChat.data = action.payload;
state.currentChat.status = 'succeeded';
})
.addCase(createChat.rejected, (state, action) => {
state.operations.status = 'failed';
state.operations.error = action.payload || action.error.message;
})
// 删除聊天
.addCase(deleteChat.pending, (state) => {
state.operations.status = 'loading';
})
.addCase(deleteChat.fulfilled, (state, action) => {
state.operations.status = 'succeeded';
state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
state.list.total -= 1;
if (state.currentChat.data && state.currentChat.data.id === action.payload) {
state.currentChat.data = null;
}
})
.addCase(deleteChat.rejected, (state, action) => {
state.operations.status = 'failed';
state.operations.error = action.payload || action.error.message;
})
// 更新聊天
.addCase(updateChat.pending, (state) => {
state.operations.status = 'loading';
})
.addCase(updateChat.fulfilled, (state, action) => {
state.operations.status = 'succeeded';
const index = state.list.items.findIndex((chat) => chat.id === action.payload.id);
if (index !== -1) {
state.list.items[index] = action.payload;
}
if (state.currentChat.data && state.currentChat.data.id === action.payload.id) {
state.currentChat.data = action.payload;
}
})
.addCase(updateChat.rejected, (state, action) => {
state.operations.status = 'failed';
state.operations.error = action.payload || action.error.message;
});
},
});
// 导出 actions
export const { resetOperationStatus, resetCurrentChat, setCurrentChat } = chatHistorySlice.actions;
// 导出 reducer
export default chatHistorySlice.reducer;

View File

@ -1,87 +0,0 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { get, post, put, del } from '../../services/api';
/**
* 获取聊天历史列表
* @param {Object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.page_size - 每页数量
*/
export const fetchChatHistory = createAsyncThunk(
'chatHistory/fetchChatHistory',
async (params = { page: 1, page_size: 10 }, { rejectWithValue }) => {
try {
const response = await get('/chat-history/', { params });
// 处理返回格式
if (response.data && response.data.code === 200) {
return response.data.data;
}
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to fetch chat history');
}
}
);
/**
* 创建新聊天
* @param {Object} chatData - 聊天数据
* @param {string} chatData.knowledge_base_id - 知识库ID
* @param {string} chatData.title - 聊天标题
*/
export const createChat = createAsyncThunk('chatHistory/createChat', async (chatData, { rejectWithValue }) => {
try {
const response = await post('/chat-history/', chatData);
// 处理返回格式
if (response.data && response.data.code === 200) {
return response.data.data.chat;
}
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to create chat');
}
});
/**
* 更新聊天
* @param {Object} params - 更新参数
* @param {string} params.id - 聊天ID
* @param {Object} params.data - 更新数据
*/
export const updateChat = createAsyncThunk('chatHistory/updateChat', async ({ id, data }, { rejectWithValue }) => {
try {
const response = await put(`/chat-history/${id}/`, data);
// 处理返回格式
if (response.data && response.data.code === 200) {
return response.data.data.chat;
}
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to update chat');
}
});
/**
* 删除聊天
* @param {string} id - 聊天ID
*/
export const deleteChat = createAsyncThunk('chatHistory/deleteChat', async (id, { rejectWithValue }) => {
try {
const response = await del(`/chat-history/${id}/`);
// 处理返回格式
if (response.data && response.data.code === 200) {
return id;
}
return id;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to delete chat');
}
});

View File

@ -0,0 +1,83 @@
import { createSlice } from '@reduxjs/toolkit';
import { fetchPermissionsThunk, approvePermissionThunk, rejectPermissionThunk } from './permissions.thunks';
const initialState = {
permissions: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
approveReject: {
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
currentId: null,
},
};
const permissionsSlice = createSlice({
name: 'permissions',
initialState,
reducers: {
resetApproveRejectStatus: (state) => {
state.approveReject = {
status: 'idle',
error: null,
currentId: null,
};
},
},
extraReducers: (builder) => {
// 获取权限申请列表
builder
.addCase(fetchPermissionsThunk.pending, (state) => {
state.permissions.status = 'loading';
state.permissions.error = null;
})
.addCase(fetchPermissionsThunk.fulfilled, (state, action) => {
state.permissions.status = 'succeeded';
state.permissions.items = action.payload;
})
.addCase(fetchPermissionsThunk.rejected, (state, action) => {
state.permissions.status = 'failed';
state.permissions.error = action.payload || '获取权限申请列表失败';
});
// 批准权限申请
builder
.addCase(approvePermissionThunk.pending, (state, action) => {
state.approveReject.status = 'loading';
state.approveReject.error = null;
state.approveReject.currentId = action.meta.arg.id;
})
.addCase(approvePermissionThunk.fulfilled, (state, action) => {
state.approveReject.status = 'succeeded';
// 从列表中移除已批准的申请
state.permissions.items = state.permissions.items.filter((item) => item.id !== action.meta.arg.id);
})
.addCase(approvePermissionThunk.rejected, (state, action) => {
state.approveReject.status = 'failed';
state.approveReject.error = action.payload || '批准权限申请失败';
});
// 拒绝权限申请
builder
.addCase(rejectPermissionThunk.pending, (state, action) => {
state.approveReject.status = 'loading';
state.approveReject.error = null;
state.approveReject.currentId = action.meta.arg.id;
})
.addCase(rejectPermissionThunk.fulfilled, (state, action) => {
state.approveReject.status = 'succeeded';
// 从列表中移除已拒绝的申请
state.permissions.items = state.permissions.items.filter((item) => item.id !== action.meta.arg.id);
})
.addCase(rejectPermissionThunk.rejected, (state, action) => {
state.approveReject.status = 'failed';
state.approveReject.error = action.payload || '拒绝权限申请失败';
});
},
});
export const { resetApproveRejectStatus } = permissionsSlice.actions;
export default permissionsSlice.reducer;

View File

@ -0,0 +1,48 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { get, post } from '../../services/api';
// 获取权限申请列表
export const fetchPermissionsThunk = createAsyncThunk(
'permissions/fetchPermissions',
async (_, { rejectWithValue }) => {
try {
const response = await get('/permissions/');
return response || [];
} catch (error) {
console.error('获取权限申请列表失败:', error);
return rejectWithValue('获取权限申请列表失败');
}
}
);
// 批准权限申请
export const approvePermissionThunk = createAsyncThunk(
'permissions/approvePermission',
async ({ id, responseMessage }, { rejectWithValue }) => {
try {
const response = await post(`/permissions/${id}/approve/`, {
response_message: responseMessage || '已批准',
});
return response;
} catch (error) {
console.error('批准权限申请失败:', error);
return rejectWithValue('批准权限申请失败');
}
}
);
// 拒绝权限申请
export const rejectPermissionThunk = createAsyncThunk(
'permissions/rejectPermission',
async ({ id, responseMessage }, { rejectWithValue }) => {
try {
const response = await post(`/permissions/${id}/reject/`, {
response_message: responseMessage || '已拒绝',
});
return response;
} catch (error) {
console.error('拒绝权限申请失败:', error);
return rejectWithValue('拒绝权限申请失败');
}
}
);

View File

@ -5,12 +5,14 @@ import notificationReducer from './notification.slice.js';
import authReducer from './auth/auth.slice.js';
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
import chatReducer from './chat/chat.slice.js';
import permissionsReducer from './permissions/permissions.slice.js';
const rootRducer = combineReducers({
auth: authReducer,
notification: notificationReducer,
knowledgeBase: knowledgeBaseReducer,
chat: chatReducer,
permissions: permissionsReducer,
});
const persistConfig = {