[dev]notificationcenter & setting

This commit is contained in:
susie-laptop 2025-03-19 22:01:09 -04:00
parent 523c474001
commit 6f48ff656b
18 changed files with 966 additions and 442 deletions

12
public/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OOIN 智能知识库</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,214 @@
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { clearNotifications } from '../store/notificationCenter/notificationCenter.slice';
import RequestDetailSlideOver from '../pages/Permissions/components/RequestDetailSlideOver';
import { approvePermissionThunk, rejectPermissionThunk } from '../store/permissions/permissions.thunks';
import { showNotification } from '../store/notification.slice';
export default function NotificationCenter({ show, onClose }) {
const [showAll, setShowAll] = useState(false);
const dispatch = useDispatch();
const { notifications } = useSelector((state) => state.notificationCenter);
const [selectedRequest, setSelectedRequest] = useState(null);
const [showSlideOver, setShowSlideOver] = useState(false);
const [showResponseInput, setShowResponseInput] = useState(false);
const [currentRequestId, setCurrentRequestId] = useState(null);
const [isApproving, setIsApproving] = useState(false);
const [responseMessage, setResponseMessage] = useState('');
const displayedNotifications = showAll ? notifications : notifications.slice(0, 2);
const handleClearAll = () => {
dispatch(clearNotifications());
};
const handleViewDetail = (notification) => {
if (notification.type === 'permission') {
setSelectedRequest(notification);
setShowSlideOver(true);
}
};
const handleCloseSlideOver = () => {
setShowSlideOver(false);
setTimeout(() => {
setSelectedRequest(null);
}, 300);
};
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));
}
};
if (!show) return null;
return (
<>
<div
className='notification-center card shadow-lg'
style={{
position: 'fixed',
top: '60px',
right: '20px',
width: '400px',
zIndex: 1050,
backgroundColor: 'white',
maxHeight: '80vh',
display: 'flex',
flexDirection: 'column',
}}
>
<div className='card-header bg-white border-0 d-flex justify-content-between align-items-center py-3'>
<h6 className='mb-0'>通知中心</h6>
<div className='d-flex gap-3 align-items-center'>
<button
className='btn btn-link text-decoration-none p-0 text-dark'
onClick={handleClearAll}
disabled={notifications.length === 0}
>
清除所有通知
</button>
<button type='button' className='btn-close' onClick={onClose}></button>
</div>
</div>
<div className='card-body p-0' style={{ overflowY: 'auto' }}>
{displayedNotifications.map((notification) => (
<div key={notification.id} className='notification-item p-3 border-bottom hover-bg-light'>
<div className='d-flex gap-3'>
<div className='notification-icon'>
<i className={`bi ${notification.icon} text-dark fs-5`}></i>
</div>
<div className='flex-grow-1'>
<div className='d-flex justify-content-between align-items-start'>
<h6 className='mb-1'>{notification.title}</h6>
<small className='text-muted'>{notification.time}</small>
</div>
<p className='mb-1 text-secondary'>{notification.content}</p>
<div className='d-flex gap-2'>
{notification.hasDetail && (
<button
className='btn btn-sm btn-dark'
onClick={() => handleViewDetail(notification)}
>
查看详情
</button>
)}
</div>
</div>
</div>
</div>
))}
</div>
<div className='card-footer bg-white border-0 text-center p-3 mt-auto'>
<button
className='btn btn-link text-decoration-none text-dark'
onClick={() => setShowAll(!showAll)}
>
{showAll ? '收起' : '查看全部通知'}
</button>
</div>
</div>
{/* 使用滑动面板组件 */}
<RequestDetailSlideOver
show={showSlideOver}
onClose={handleCloseSlideOver}
request={selectedRequest}
onApprove={(id) => handleOpenResponseInput(id, true)}
onReject={(id) => handleOpenResponseInput(id, false)}
processingId={currentRequestId}
approveRejectStatus={showResponseInput ? 'loading' : 'idle'}
isApproving={isApproving}
/>
{/* 回复输入弹窗 */}
{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={showResponseInput}
></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={showResponseInput}
>
取消
</button>
<button
type='button'
className={`btn ${isApproving ? 'btn-success' : 'btn-danger'}`}
onClick={handleProcessRequest}
disabled={showResponseInput}
>
{showResponseInput ? (
<>
<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,101 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import '../styles/style.scss';
export default function UserSettingsModal({ show, onClose }) {
const { user } = useSelector((state) => state.auth);
const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
if (!show) return null;
return (
<div className='modal show d-block' style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className='modal-dialog modal-dialog-centered'>
<div className='modal-content'>
<div className='modal-header border-0'>
<h5 className='modal-title'>管理员个人设置</h5>
<button type='button' className='btn-close' onClick={onClose}></button>
</div>
<div className='modal-body'>
<div className='mb-4'>
<h6 className='text-secondary mb-3'>个人信息</h6>
<div className='mb-3'>
<label className='form-label text-secondary'>姓名</label>
<input type='text' className='form-control' value={user?.username || ''} readOnly />
</div>
<div className='mb-3'>
<label className='form-label text-secondary'>邮箱</label>
<input
type='email'
className='form-control'
value={user?.email || 'admin@ooin.com'}
readOnly
/>
</div>
</div>
<div className='mb-4'>
<h6 className='text-secondary mb-3'>安全设置</h6>
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
<div>
<div className='d-flex align-items-center gap-2'>
<i className='bi bi-key'></i>
<span>修改密码</span>
</div>
<small className='text-secondary'>上次修改{lastPasswordChange}</small>
</div>
<button className='btn btn-outline-dark btn-sm'>修改</button>
</div>
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded mt-3'>
<div>
<div className='d-flex align-items-center gap-2'>
<i className='bi bi-shield-check'></i>
<span>双重认证</span>
</div>
<small className='text-secondary'>增强账户安全性</small>
</div>
<button className='btn btn-outline-dark btn-sm'>设置</button>
</div>
</div>
<div>
<h6 className='text-secondary mb-3'>通知设置</h6>
<div className='form-check form-switch mb-3 dark-switch'>
<input
className='form-check-input'
type='checkbox'
id='notificationSwitch1'
defaultChecked
/>
<label className='form-check-label' htmlFor='notificationSwitch1'>
访问请求通知
</label>
<div className='text-secondary small'>新的数据集访问申请通知</div>
</div>
<div className='form-check form-switch dark-switch'>
<input
className='form-check-input'
type='checkbox'
id='notificationSwitch2'
defaultChecked
/>
<label className='form-check-label' htmlFor='notificationSwitch2'>
安全提醒
</label>
<div className='text-secondary small'>异常登录和权限变更提醒</div>
</div>
</div>
</div>
<div className='modal-footer border-0'>
<button type='button' className='btn btn-outline-dark' onClick={onClose}>
取消
</button>
<button type='button' className='btn btn-dark'>
保存更改
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -101,5 +101,7 @@ export const icons = {
chat: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M123.6 391.3c12.9-9.4 29.6-11.8 44.6-6.4c26.5 9.6 56.2 15.1 87.8 15.1c124.7 0 208-80.5 208-160s-83.3-160-208-160S48 160.5 48 240c0 32 12.4 62.8 35.7 89.2c8.6 9.7 12.8 22.5 11.8 35.5c-1.4 18.1-5.7 34.7-11.3 49.4c17-7.9 31.1-16.7 39.4-22.7zM21.2 431.9c1.8-2.7 3.5-5.4 5.1-8.1c10-16.6 19.5-38.4 21.4-62.9C17.7 326.8 0 285.1 0 240C0 125.1 114.6 32 256 32s256 93.1 256 208s-114.6 208-256 208c-37.1 0-72.3-6.4-104.1-17.9c-11.9 8.7-31.3 20.6-54.3 30.6c-15.1 6.6-32.3 12.6-50.1 16.1c-.8 .2-1.6 .3-2.4 .5c-4.4 .8-8.7 1.5-13.2 1.9c-.2 0-.5 .1-.7 .1c-5.1 .5-10.2 .8-15.3 .8c-6.5 0-12.3-3.9-14.8-9.9c-2.5-6-1.1-12.8 3.4-17.4c4.1-4.2 7.8-8.7 11.3-13.5c1.7-2.3 3.3-4.6 4.8-6.9l.3-.5z"/></svg>`,
'arrowup-upload': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill='currentColor'><path d="M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3 192 320c0 17.7 14.3 32 32 32s32-14.3 32-32l0-210.7 73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 17.7-14.3 32-32 32L96 448c-17.7 0-32-14.3-32-32l0-64z"/></svg>`,
send: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z"/></svg>`,
search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>`
search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>`,
bell: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill='currentColor'><path d="M224 0c-17.7 0-32 14.3-32 32l0 19.2C119 66 64 130.6 64 208l0 18.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S19.4 416 32 416l384 0c12.6 0 24-7.4 29.2-18.9s3.1-25-5.3-34.4l-7.4-8.3C401.3 319.2 384 273.9 384 226.8l0-18.8c0-77.4-55-142-128-156.8L256 32c0-17.7-14.3-32-32-32zm45.3 493.3c12-12 18.7-28.3 18.7-45.3l-64 0-64 0c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z"/></svg>`,
'magnifying-glass': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>`
};

View File

@ -1,13 +1,19 @@
import React from 'react';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { logoutThunk } from '../store/auth/auth.thunk';
import UserSettingsModal from '../components/UserSettingsModal';
import NotificationCenter from '../components/NotificationCenter';
import SvgIcon from '../components/SvgIcon';
export default function HeaderWithNav() {
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
const { user } = useSelector((state) => state.auth);
const [showSettings, setShowSettings] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const { notifications } = useSelector((state) => state.notificationCenter);
const handleLogout = async () => {
try {
@ -27,8 +33,8 @@ export default function HeaderWithNav() {
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'>
<header>
<nav className='navbar navbar-expand-lg bg-white shadow-sm'>
<div className='container-fluid'>
<Link className='navbar-brand' to='/'>
OOIN 智能知识库
@ -74,43 +80,57 @@ export default function HeaderWithNav() {
)}
</ul>
{!!user ? (
<div className='flex-shrink-0 dropdown'>
<a
href='#'
className='d-block link-dark text-decoration-none dropdown-toggle'
data-bs-toggle='dropdown'
aria-expanded='false'
>
Hi, {user.username}
</a>
<ul
className='dropdown-menu text-small shadow'
style={{
position: 'absolute',
inset: '0px 0px auto auto',
margin: '0px',
transform: 'translate(0px, 34px)',
}}
>
{/* <li>
<Link className='dropdown-item' to='#'>
Settings
</Link>
</li> */}
{/* <li>
<Link className='dropdown-item' to='#'>
Profile
</Link>
</li> */}
<li>
<hr className='dropdown-divider' />
</li>
<li>
<Link className='dropdown-item' to='#' onClick={handleLogout}>
Sign out
</Link>
</li>
</ul>
<div className='d-flex align-items-center gap-3'>
<div className='position-relative'>
<button
className='btn btn-link text-dark p-0'
onClick={() => setShowNotifications(!showNotifications)}
>
<SvgIcon className={'bell'} />
{notifications.length > 0 && (
<span className='position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger'>
{notifications.length}
</span>
)}
</button>
</div>
<div className='flex-shrink-0 dropdown'>
<a
href='#'
className='d-block link-dark text-decoration-none dropdown-toggle'
data-bs-toggle='dropdown'
aria-expanded='false'
>
Hi, {user.username}
</a>
<ul
className='dropdown-menu text-small shadow'
style={{
position: 'absolute',
inset: '0px 0px auto auto',
margin: '0px',
transform: 'translate(0px, 34px)',
}}
>
<li>
<Link
className='dropdown-item'
to='#'
onClick={() => setShowSettings(true)}
>
个人设置
</Link>
</li>
<li>
<hr className='dropdown-divider' />
</li>
<li>
<Link className='dropdown-item' to='#' onClick={handleLogout}>
退出登录
</Link>
</li>
</ul>
</div>
</div>
) : (
<>
@ -132,6 +152,8 @@ export default function HeaderWithNav() {
</div>
</div>
</nav>
<UserSettingsModal show={showSettings} onClose={() => setShowSettings(false)} />
<NotificationCenter show={showNotifications} onClose={() => setShowNotifications(false)} />
</header>
);
}

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchMessages, sendMessage } from '../../store/chat/chat.messages.thunks';
import { resetMessages, resetSendMessageStatus } from '../../store/chat/chat.slice';
import { resetMessages, resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice';
import { showNotification } from '../../store/notification.slice';
import SvgIcon from '../../components/SvgIcon';
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
@ -65,9 +65,20 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
if (!inputMessage.trim() || sendStatus === 'loading') return;
//
dispatch(sendMessage({ chatId, content: inputMessage }));
//
const userMessage = {
id: Date.now(),
content: inputMessage,
sender: 'user',
timestamp: new Date().toISOString(),
};
dispatch(addMessage(userMessage));
//
setInputMessage('');
//
dispatch(sendMessage({ chatId, content: inputMessage }));
};
//
@ -125,19 +136,22 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
} mb-3`}
>
<div
className={`p-3 rounded-3 ${
message.sender === 'user' ? 'bg-primary text-white' : 'bg-white border'
className={`chat-message p-3 rounded-3 ${
message.sender === 'user' ? 'bg-dark text-white' : 'bg-light'
}`}
style={{ maxWidth: '75%' }}
style={{
maxWidth: '75%',
position: 'relative',
}}
>
{message.content}
<div className='message-content'>{message.content}</div>
</div>
</div>
))}
{sendStatus === 'loading' && (
<div className='d-flex justify-content-start mb-3'>
<div className='p-3 rounded-3 bg-white border'>
<div className='chat-message p-3 rounded-3 bg-light'>
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>

View File

@ -56,7 +56,7 @@ export default function NewChat() {
return (
<div className='container-fluid px-4 py-5'>
<h4 className='mb-4'>选择知识库开始聊天</h4>
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4'>
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
{readableKnowledgeBases.length > 0 ? (
readableKnowledgeBases.map((kb) => (
<div key={kb.id} className='col'>

View File

@ -19,9 +19,9 @@ export default function PermissionsPage() {
return (
<div className='permissions-container'>
<div className='api-mode-control mb-3'>
{/* <div className='api-mode-control mb-3'>
<ApiModeSwitch />
</div>
</div> */}
<div className='permissions-section mb-4'>
<PendingRequests />

View File

@ -1,20 +1,23 @@
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { showNotification } from '../../../store/notification.slice';
import {
fetchPermissionsThunk,
approvePermissionThunk,
rejectPermissionThunk,
} from '../../../store/permissions/permissions.thunks';
import { resetApproveRejectStatus } from '../../../store/permissions/permissions.slice';
import { resetOperationStatus } from '../../../store/permissions/permissions.slice';
import './PendingRequests.css'; // CSS
import SvgIcon from '../../../components/SvgIcon';
import RequestDetailSlideOver from './RequestDetailSlideOver';
//
const PAGE_SIZE = 5;
export default function PendingRequests() {
const dispatch = useDispatch();
const location = useLocation();
const [responseMessage, setResponseMessage] = useState('');
const [showResponseInput, setShowResponseInput] = useState(false);
const [currentRequestId, setCurrentRequestId] = useState(null);
@ -59,6 +62,17 @@ export default function PendingRequests() {
fetchData();
}, [dispatch]);
//
useEffect(() => {
if (location.state?.showRequestDetail) {
const { requestId, requestData } = location.state;
setSelectedRequest(requestData);
setShowSlideOver(true);
// location state
window.history.replaceState({}, document.title);
}
}, [location.state]);
// /
useEffect(() => {
if (approveRejectStatus === 'succeeded') {
@ -87,7 +101,7 @@ export default function PendingRequests() {
}
//
dispatch(resetApproveRejectStatus());
dispatch(resetOperationStatus());
} else if (approveRejectStatus === 'failed') {
dispatch(
showNotification({
@ -96,7 +110,7 @@ export default function PendingRequests() {
})
);
//
dispatch(resetApproveRejectStatus());
dispatch(resetOperationStatus());
}
}, [
approveRejectStatus,
@ -286,7 +300,7 @@ export default function PendingRequests() {
</div>
<div className='request-actions'>
<button
className='btn btn-outline-danger'
className='btn btn-outline-danger btn-sm'
onClick={(e) => {
e.stopPropagation();
handleDirectProcess(request.id, false);
@ -298,7 +312,7 @@ export default function PendingRequests() {
: '拒绝'}
</button>
<button
className='btn btn-success'
className='btn btn-outline-success btn-sm'
onClick={(e) => {
e.stopPropagation();
handleDirectProcess(request.id, true);
@ -317,113 +331,17 @@ export default function PendingRequests() {
{/* 分页控件 */}
{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.charAt(0)}
</div>
<div>
<h5 className='mb-1'>{selectedRequest.applicant}</h5>
</div>
</div>
</div>
<div className='mb-4'>
<h6 className='text-muted mb-2'>知识库信息</h6>
<p className='mb-1'>
<strong>ID</strong> {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>
)}
{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>
<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>
{/* 使用新的滑动面板组件 */}
<RequestDetailSlideOver
show={showSlideOver}
onClose={handleCloseSlideOver}
request={selectedRequest}
onApprove={(id) => handleOpenResponseInput(id, true)}
onReject={(id) => handleOpenResponseInput(id, false)}
processingId={processingId}
approveRejectStatus={approveRejectStatus}
isApproving={isApproving}
/>
{/* 回复输入弹窗 */}
{showResponseInput && (

View File

@ -0,0 +1,124 @@
import React from 'react';
import SvgIcon from '../../../components/SvgIcon';
export default function RequestDetailSlideOver({
show,
onClose,
request,
onApprove,
onReject,
processingId,
approveRejectStatus,
isApproving,
}) {
if (!request) return null;
//
const applicant = request.applicant || request.title || '未知用户';
const applicantInitial = applicant.charAt(0);
return (
<>
<div className={`slide-over-backdrop ${show ? 'show' : ''}`} onClick={onClose}></div>
<div className={`slide-over ${show ? 'show' : ''}`}>
<div className='slide-over-content'>
<div className='slide-over-header'>
<h5 className='mb-0'>申请详情</h5>
<button type='button' className='btn-close' onClick={onClose}></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'>{applicantInitial}</div>
<div>
<h5 className='mb-1'>{applicant}</h5>
</div>
</div>
</div>
<div className='mb-4'>
<h6 className='text-muted mb-2'>知识库信息</h6>
<p className='mb-1'>
<strong>ID</strong> {request.knowledge_base || request.content || '未知知识库'}
</p>
</div>
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请权限</h6>
<div className='d-flex flex-wrap gap-2 mb-3'>
{request.permissions?.can_read && !request.permissions?.can_edit && (
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
只读
</span>
)}
{request.permissions?.can_edit && request.permissions?.can_delete && (
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
完全访问
</span>
)}
</div>
</div>
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请时间</h6>
<p className='mb-1'>{new Date(request.created_at || Date.now()).toLocaleString()}</p>
</div>
{request.expires_at && (
<div className='mb-4'>
<h6 className='text-muted mb-2'>到期时间</h6>
<p className='mb-1'>{new Date(request.expires_at).toLocaleString()}</p>
</div>
)}
<div className='mb-4'>
<h6 className='text-muted mb-2'>申请理由</h6>
<div className='p-3 bg-light rounded'>
{request.reason || request.content || '无申请理由'}
</div>
</div>
</div>
<div className='slide-over-footer'>
<button
className='btn btn-outline-danger me-2'
onClick={() => onReject(request.id)}
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-outline-success'
onClick={() => onApprove(request.id)}
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>
</>
);
}

View File

@ -1,119 +1,15 @@
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { get, put } from '../../../services/api';
import { showNotification } from '../../../store/notification.slice';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUserPermissions, updateUserPermissions } from '../../../store/permissions/permissions.thunks';
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();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedUser, setSelectedUser] = useState(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
@ -123,40 +19,13 @@ export default function UserPermissions() {
const [pageSize, setPageSize] = useState(5);
const [totalPages, setTotalPages] = useState(1);
// Redux store
const { items: users, status: loading, error } = useSelector((state) => state.permissions.users);
//
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await get('/users/permissions/');
if (response && response.code === 200) {
// API使
const apiUsers = response.data.users || [];
if (apiUsers.length > 0) {
setUsers(apiUsers);
} else {
console.log('API返回的用户列表为空使用模拟数据');
setUsers(mockUsers);
}
} else {
// API使
console.log('API请求失败使用模拟数据');
setUsers(mockUsers);
}
} catch (error) {
console.error('获取用户列表失败:', error);
setError('获取用户列表失败');
// API使
console.log('API请求出错使用模拟数据作为后备');
setUsers(mockUsers);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
dispatch(fetchUserPermissions());
}, [dispatch]);
//
useEffect(() => {
@ -183,52 +52,10 @@ export default function UserPermissions() {
//
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',
})
);
}
await dispatch(updateUserPermissions({ userId, permissions: updatedPermissions })).unwrap();
handleCloseDetailsModal();
} catch (error) {
console.error('权限更新失败:', error);
dispatch(
showNotification({
message: '权限更新失败',
type: 'danger',
})
);
console.error('更新权限失败:', error);
}
};
@ -253,7 +80,6 @@ export default function UserPermissions() {
//
const getFilteredUsers = () => {
if (!searchTerm.trim()) return users;
return users.filter(
(user) =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
@ -278,7 +104,7 @@ export default function UserPermissions() {
return (
<div className='d-flex justify-content-center align-items-center mt-4'>
<nav aria-label='用户权限分页'>
<ul className='pagination'>
<ul className='pagination dark-pagination'>
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
<button
className='page-link'
@ -324,7 +150,7 @@ export default function UserPermissions() {
};
//
if (loading && users.length === 0) {
if (loading === 'loading' && users.length === 0) {
return (
<div className='text-center py-5'>
<div className='spinner-border' role='status'>
@ -373,7 +199,7 @@ export default function UserPermissions() {
onChange={handleSearchChange}
/>
<span className='input-group-text'>
<i className='bi bi-search'></i>
<SvgIcon className='magnifying-glass' />
</span>
</div>
</div>
@ -427,7 +253,7 @@ export default function UserPermissions() {
</span>
)}
{user.permissions_count.admin > 0 && (
<span className='badge bg-danger d-flex align-items-center gap-1'>
<span className='badge bg-dark-subtle d-flex align-items-center gap-1'>
无访问权限: {user.permissions_count.admin}
</span>
)}

View File

@ -610,12 +610,10 @@ export const mockGet = async (url, config = {}) => {
}
return {
code: 200,
message: '获取成功',
data: {
code: 200,
message: '获取成功',
data: {
messages: chatMessages[chatId] || [],
},
messages: chatMessages[chatId] || [],
},
};
}
@ -631,23 +629,19 @@ export const mockGet = async (url, config = {}) => {
);
const result = paginate(filtered, page_size, page);
return {
data: {
code: 200,
message: 'success',
data: result,
},
code: 200,
message: 'success',
data: result,
};
}
// 用户权限管理 - 获取用户列表
if (url === '/users/permissions/') {
return {
code: 200,
message: 'success',
data: {
code: 200,
message: 'success',
data: {
users: mockUsers,
},
users: mockUsers,
},
};
}
@ -655,13 +649,11 @@ export const mockGet = async (url, config = {}) => {
// 用户权限管理 - 获取待处理申请
if (url === '/permissions/pending/') {
return {
code: 200,
message: 'success',
data: {
code: 200,
message: 'success',
data: {
items: mockPendingRequests,
total: mockPendingRequests.length,
},
items: mockPendingRequests,
total: mockPendingRequests.length,
},
};
}
@ -671,12 +663,10 @@ export const mockGet = async (url, config = {}) => {
const userId = url.match(/\/users\/(.+)\/permissions\//)[1];
return {
code: 200,
message: 'success',
data: {
code: 200,
message: 'success',
data: {
permissions: mockUserPermissions[userId] || [],
},
permissions: mockUserPermissions[userId] || [],
},
};
}
@ -758,13 +748,11 @@ export const mockPost = async (url, data) => {
knowledgeBases.push(newKnowledgeBase);
return {
code: 200,
message: '知识库创建成功',
data: {
code: 200,
message: '知识库创建成功',
data: {
knowledge_base: newKnowledgeBase,
external_id: uuidv4(),
},
knowledge_base: newKnowledgeBase,
external_id: uuidv4(),
},
};
}
@ -773,12 +761,10 @@ export const mockPost = async (url, data) => {
if (url === '/chat-history/') {
const newChat = mockCreateChat(data);
return {
code: 200,
message: 'success',
data: {
code: 200,
message: 'success',
data: {
chat: newChat,
},
chat: newChat,
},
};
}
@ -826,13 +812,11 @@ export const mockPost = async (url, data) => {
}
return {
code: 200,
message: '发送成功',
data: {
code: 200,
message: '发送成功',
data: {
user_message: userMessage,
bot_message: botMessage,
},
user_message: userMessage,
bot_message: botMessage,
},
};
}

View File

@ -77,6 +77,11 @@ const chatSlice = createSlice({
state.sendMessage.status = 'idle';
state.sendMessage.error = null;
},
// 添加消息
addMessage: (state, action) => {
state.messages.items.push(action.payload);
},
},
extraReducers: (builder) => {
// 获取聊天列表
@ -151,6 +156,7 @@ const chatSlice = createSlice({
// 获取聊天消息
.addCase(fetchMessages.pending, (state) => {
state.messages.status = 'loading';
state.messages.error = null;
})
.addCase(fetchMessages.fulfilled, (state, action) => {
state.messages.status = 'succeeded';
@ -158,33 +164,40 @@ const chatSlice = createSlice({
})
.addCase(fetchMessages.rejected, (state, action) => {
state.messages.status = 'failed';
state.messages.error = action.payload || action.error.message;
state.messages.error = action.error.message;
})
// 发送聊天消息
.addCase(sendMessage.pending, (state) => {
state.sendMessage.status = 'loading';
state.sendMessage.error = null;
})
.addCase(sendMessage.fulfilled, (state, action) => {
state.sendMessage.status = 'succeeded';
// 添加用户消息和机器人回复
if (action.payload.user_message) {
state.messages.items.push(action.payload.user_message);
}
if (action.payload.bot_message) {
state.messages.items.push(action.payload.bot_message);
// 更新消息列表
const index = state.messages.items.findIndex(
(msg) => msg.content === action.payload.content && msg.sender === action.payload.sender
);
if (index === -1) {
state.messages.items.push(action.payload);
}
})
.addCase(sendMessage.rejected, (state, action) => {
state.sendMessage.status = 'failed';
state.sendMessage.error = action.payload || action.error.message;
state.sendMessage.error = action.error.message;
});
},
});
// 导出 actions
export const { resetOperationStatus, resetCurrentChat, setCurrentChat, resetMessages, resetSendMessageStatus } =
chatSlice.actions;
export const {
resetOperationStatus,
resetCurrentChat,
setCurrentChat,
resetMessages,
resetSendMessageStatus,
addMessage,
} = chatSlice.actions;
// 导出 reducer
export default chatSlice.reducer;

View File

@ -0,0 +1,64 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
notifications: [
{
id: 1,
type: 'permission',
icon: 'bi-shield',
title: '新的权限请求',
content: '张三请求访问销售数据集',
time: '10分钟前',
hasDetail: true,
},
{
id: 2,
type: 'system',
icon: 'bi-info-circle',
title: '系统更新通知',
content: '系统将在今晚23:00进行例行维护',
time: '1小时前',
hasDetail: false,
},
{
id: 3,
type: 'permission',
icon: 'bi-shield',
title: '新的权限请求',
content: '李四请求访问用户数据集',
time: '2小时前',
hasDetail: true,
},
{
id: 4,
type: 'system',
icon: 'bi-exclamation-circle',
title: '安全提醒',
content: '检测到异常登录行为,请及时查看',
time: '3小时前',
hasDetail: true,
},
{
id: 5,
type: 'permission',
icon: 'bi-shield',
title: '权限变更通知',
content: '管理员修改了您的数据访问权限',
time: '1天前',
hasDetail: true,
},
],
};
const notificationCenterSlice = createSlice({
name: 'notificationCenter',
initialState,
reducers: {
clearNotifications: (state) => {
state.notifications = [];
},
},
});
export const { clearNotifications } = notificationCenterSlice.actions;
export default notificationCenterSlice.reducer;

View File

@ -1,14 +1,29 @@
import { createSlice } from '@reduxjs/toolkit';
import { fetchPermissionsThunk, approvePermissionThunk, rejectPermissionThunk } from './permissions.thunks';
import {
fetchUserPermissions,
updateUserPermissions,
fetchPermissionsThunk,
approvePermissionThunk,
rejectPermissionThunk,
} from './permissions.thunks';
const initialState = {
permissions: {
users: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
operations: {
status: 'idle',
error: null,
},
pending: {
items: [],
status: 'idle',
error: null,
},
approveReject: {
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
status: 'idle',
error: null,
currentId: null,
},
@ -18,66 +33,95 @@ const permissionsSlice = createSlice({
name: 'permissions',
initialState,
reducers: {
resetApproveRejectStatus: (state) => {
state.approveReject = {
status: 'idle',
error: null,
currentId: null,
};
resetOperationStatus: (state) => {
state.operations.status = 'idle';
state.operations.error = null;
},
},
extraReducers: (builder) => {
// 获取权限申请列表
builder
// 获取用户权限列表
.addCase(fetchUserPermissions.pending, (state) => {
state.users.status = 'loading';
state.users.error = null;
})
.addCase(fetchUserPermissions.fulfilled, (state, action) => {
state.users.status = 'succeeded';
state.users.items = action.payload;
})
.addCase(fetchUserPermissions.rejected, (state, action) => {
state.users.status = 'failed';
state.users.error = action.error.message;
})
// 更新用户权限
.addCase(updateUserPermissions.pending, (state) => {
state.operations.status = 'loading';
state.operations.error = null;
})
.addCase(updateUserPermissions.fulfilled, (state, action) => {
state.operations.status = 'succeeded';
// 更新用户列表中的权限信息
const index = state.users.items.findIndex((user) => user.id === action.payload.userId);
if (index !== -1) {
state.users.items[index] = {
...state.users.items[index],
permissions: action.payload.permissions,
};
}
})
.addCase(updateUserPermissions.rejected, (state, action) => {
state.operations.status = 'failed';
state.operations.error = action.error.message;
})
// 获取待处理申请列表
.addCase(fetchPermissionsThunk.pending, (state) => {
state.permissions.status = 'loading';
state.permissions.error = null;
state.pending.status = 'loading';
state.pending.error = null;
})
.addCase(fetchPermissionsThunk.fulfilled, (state, action) => {
state.permissions.status = 'succeeded';
state.permissions.items = action.payload;
state.pending.status = 'succeeded';
state.pending.items = action.payload;
})
.addCase(fetchPermissionsThunk.rejected, (state, action) => {
state.permissions.status = 'failed';
state.permissions.error = action.payload || '获取权限申请列表失败';
});
state.pending.status = 'failed';
state.pending.error = action.error.message;
})
// 批准权限申请
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) => {
.addCase(approvePermissionThunk.fulfilled, (state) => {
state.approveReject.status = 'succeeded';
// 从列表中移除已批准的申请
state.permissions.items = state.permissions.items.filter((item) => item.id !== action.meta.arg.id);
state.approveReject.currentId = null;
})
.addCase(approvePermissionThunk.rejected, (state, action) => {
state.approveReject.status = 'failed';
state.approveReject.error = action.payload || '批准权限申请失败';
});
state.approveReject.error = action.error.message;
state.approveReject.currentId = null;
})
// 拒绝权限申请
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) => {
.addCase(rejectPermissionThunk.fulfilled, (state) => {
state.approveReject.status = 'succeeded';
// 从列表中移除已拒绝的申请
state.permissions.items = state.permissions.items.filter((item) => item.id !== action.meta.arg.id);
state.approveReject.currentId = null;
})
.addCase(rejectPermissionThunk.rejected, (state, action) => {
state.approveReject.status = 'failed';
state.approveReject.error = action.payload || '拒绝权限申请失败';
state.approveReject.error = action.error.message;
state.approveReject.currentId = null;
});
},
});
export const { resetApproveRejectStatus } = permissionsSlice.actions;
export const { resetOperationStatus } = permissionsSlice.actions;
export default permissionsSlice.reducer;

View File

@ -1,14 +1,16 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { get, post } from '../../services/api';
import { get, post, put } from '../../services/api';
import { showNotification } from '../notification.slice';
// 获取权限申请列表
export const fetchPermissionsThunk = createAsyncThunk(
'permissions/fetchPermissions',
async (_, { rejectWithValue }) => {
try {
const response = await get('/permissions/pending/');
if (response?.data?.code === 200) {
return response.data.data.items || [];
const { data, message, code } = await get('/permissions/pending/');
if (code === 200) {
return data.items || [];
}
return rejectWithValue('获取权限申请列表失败');
} catch (error) {
@ -49,3 +51,88 @@ export const rejectPermissionThunk = createAsyncThunk(
}
}
);
// 生成模拟数据
const generateMockUsers = () => {
const users = [];
const userNames = [
{ username: 'zhangsan', name: '张三', department: '达人组', position: '达人对接' },
{ username: 'lisi', name: '李四', department: '达人组', position: '达人对接' },
{ username: 'wangwu', name: '王五', department: '达人组', position: '达人对接' },
{ username: 'zhaoliu', name: '赵六', department: '达人组', position: '达人对接' },
{ username: 'qianqi', name: '钱七', department: '达人组', position: '达人对接' },
{ username: 'sunba', name: '孙八', department: '达人组', position: '达人对接' },
{ username: 'zhoujiu', name: '周九', department: '达人组', position: '达人对接' },
{ username: 'wushi', name: '吴十', department: '达人组', position: '达人对接' },
];
for (let i = 1; i <= 20; i++) {
const randomUser = userNames[Math.floor(Math.random() * userNames.length)];
const hasAdminPermission = Math.random() > 0.8; // 20%的概率有管理员权限
const hasEditPermission = Math.random() > 0.5; // 50%的概率有编辑权限
const hasReadPermission = hasAdminPermission || hasEditPermission || Math.random() > 0.3; // 如果有管理员或编辑权限,一定有读取权限
users.push({
id: i.toString(),
username: randomUser.username,
name: randomUser.name,
department: randomUser.department,
position: randomUser.position,
permissions_count: {
read: hasReadPermission ? 1 : 0,
edit: hasEditPermission ? 1 : 0,
admin: hasAdminPermission ? 1 : 0,
},
});
}
return users;
};
// 获取用户权限列表
export const fetchUserPermissions = createAsyncThunk(
'permissions/fetchUserPermissions',
async (_, { rejectWithValue }) => {
try {
// 模拟API延迟
await new Promise((resolve) => setTimeout(resolve, 1000));
// 返回模拟数据
return generateMockUsers();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// 更新用户权限
export const updateUserPermissions = createAsyncThunk(
'permissions/updateUserPermissions',
async ({ userId, permissions }, { dispatch, rejectWithValue }) => {
try {
const response = await put(`/users/${userId}/permissions/`, { permissions });
if (response && response.code === 200) {
dispatch(
showNotification({
message: '权限更新成功',
type: 'success',
})
);
return {
userId,
permissions: response.data.permissions,
};
}
return rejectWithValue(response?.message || '更新权限失败');
} catch (error) {
dispatch(
showNotification({
message: error.message || '更新权限失败',
type: 'danger',
})
);
return rejectWithValue(error.message);
}
}
);

View File

@ -6,6 +6,7 @@ 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';
import notificationCenterReducer from './notificationCenter/notificationCenter.slice.js';
const rootRducer = combineReducers({
auth: authReducer,
@ -13,6 +14,7 @@ const rootRducer = combineReducers({
knowledgeBase: knowledgeBaseReducer,
chat: chatReducer,
permissions: permissionsReducer,
notificationCenter: notificationCenterReducer,
});
const persistConfig = {

View File

@ -47,3 +47,100 @@
min-width: 300px !important;
}
}
/* 自定义黑色系开关样式 */
.dark-switch .form-check-input {
border: 1px solid #dee2e6;
background-color: #fff; /* 关闭状态背景色 */
}
/* 关闭状态滑块 */
.dark-switch .form-check-input:not(:checked) {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle fill='%23adb5bd' r='3'/></svg>");
}
/* 打开状态 */
.dark-switch .form-check-input:checked {
background-color: #000; /* 打开状态背景色 */
border-color: #000;
}
/* 打开状态滑块 */
.dark-switch .form-check-input:checked {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle fill='%23fff' r='3'/></svg>");
}
/* 悬停效果 */
.dark-switch .form-check-input:hover {
filter: brightness(0.9);
}
/* 禁用状态 */
.dark-switch .form-check-input:disabled {
opacity: 0.5;
background-color: #e9ecef;
}
// 通知中心样式
.notification-item {
transition: background-color 0.2s ease;
&:hover {
background-color: $gray-100;
}
}
// 黑色主题的开关按钮
.form-check-input:checked {
background-color: $dark;
border-color: $dark;
}
/* 自定义分页样式 */
.dark-pagination {
margin: 0;
}
.dark-pagination .page-link {
color: #000; /* 默认文字颜色 */
background-color: #fff; /* 默认背景 */
border: 1px solid #dee2e6; /* 边框颜色 */
transition: all 0.3s ease; /* 平滑过渡效果 */
}
/* 激活状态 */
.dark-pagination .page-item.active .page-link {
background-color: #000 !important;
border-color: #000;
color: #fff !important;
}
/* 悬停状态 */
.dark-pagination .page-link:hover {
background-color: #f8f9fa; /* 浅灰背景 */
border-color: #adb5bd;
}
/* 禁用状态 */
.dark-pagination .page-item.disabled .page-link {
color: #6c757d !important;
background-color: #e9ecef !important;
border-color: #dee2e6;
pointer-events: none;
opacity: 0.7;
}
/* 自定义下拉框 */
.dark-select {
border: 1px solid #000 !important;
color: #000 !important;
}
.dark-select:focus {
box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.25); /* 黑色聚焦阴影 */
}
/* 下拉箭头颜色 */
.dark-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='%23000' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
}