mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-07 22:28:13 +08:00
[dev]notificationcenter & setting
This commit is contained in:
parent
523c474001
commit
6f48ff656b
12
public/index.html
Normal file
12
public/index.html
Normal 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>
|
214
src/components/NotificationCenter.jsx
Normal file
214
src/components/NotificationCenter.jsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
101
src/components/UserSettingsModal.jsx
Normal file
101
src/components/UserSettingsModal.jsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>`
|
||||
};
|
||||
|
@ -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,6 +80,20 @@ export default function HeaderWithNav() {
|
||||
)}
|
||||
</ul>
|
||||
{!!user ? (
|
||||
<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='#'
|
||||
@ -92,26 +112,26 @@ export default function HeaderWithNav() {
|
||||
transform: 'translate(0px, 34px)',
|
||||
}}
|
||||
>
|
||||
{/* <li>
|
||||
<Link className='dropdown-item' to='#'>
|
||||
Settings
|
||||
<li>
|
||||
<Link
|
||||
className='dropdown-item'
|
||||
to='#'
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
个人设置
|
||||
</Link>
|
||||
</li> */}
|
||||
{/* <li>
|
||||
<Link className='dropdown-item' to='#'>
|
||||
Profile
|
||||
</Link>
|
||||
</li> */}
|
||||
</li>
|
||||
<li>
|
||||
<hr className='dropdown-divider' />
|
||||
</li>
|
||||
<li>
|
||||
<Link className='dropdown-item' to='#' onClick={handleLogout}>
|
||||
Sign out
|
||||
退出登录
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<hr className='d-lg-none' />
|
||||
@ -132,6 +152,8 @@ export default function HeaderWithNav() {
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<UserSettingsModal show={showSettings} onClose={() => setShowSettings(false)} />
|
||||
<NotificationCenter show={showNotifications} onClose={() => setShowNotifications(false)} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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'>
|
||||
|
@ -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 />
|
||||
|
@ -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 && (
|
||||
|
124
src/pages/Permissions/components/RequestDetailSlideOver.jsx
Normal file
124
src/pages/Permissions/components/RequestDetailSlideOver.jsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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',
|
||||
})
|
||||
);
|
||||
|
||||
// 关闭弹窗
|
||||
await dispatch(updateUserPermissions({ userId, permissions: updatedPermissions })).unwrap();
|
||||
handleCloseDetailsModal();
|
||||
} else {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '权限更新失败',
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
}
|
||||
} 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>
|
||||
)}
|
||||
|
@ -610,13 +610,11 @@ export const mockGet = async (url, config = {}) => {
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
messages: chatMessages[chatId] || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -631,38 +629,32 @@ export const mockGet = async (url, config = {}) => {
|
||||
);
|
||||
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: {
|
||||
items: mockPendingRequests,
|
||||
total: mockPendingRequests.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -671,13 +663,11 @@ export const mockGet = async (url, config = {}) => {
|
||||
const userId = url.match(/\/users\/(.+)\/permissions\//)[1];
|
||||
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
permissions: mockUserPermissions[userId] || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -758,14 +748,12 @@ export const mockPost = async (url, data) => {
|
||||
knowledgeBases.push(newKnowledgeBase);
|
||||
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: '知识库创建成功',
|
||||
data: {
|
||||
knowledge_base: newKnowledgeBase,
|
||||
external_id: uuidv4(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -773,13 +761,11 @@ export const mockPost = async (url, data) => {
|
||||
if (url === '/chat-history/') {
|
||||
const newChat = mockCreateChat(data);
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
chat: newChat,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -826,14 +812,12 @@ export const mockPost = async (url, data) => {
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: '发送成功',
|
||||
data: {
|
||||
user_message: userMessage,
|
||||
bot_message: botMessage,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
64
src/store/notificationCenter/notificationCenter.slice.js
Normal file
64
src/store/notificationCenter/notificationCenter.slice.js
Normal 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;
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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 = {
|
||||
|
@ -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");
|
||||
}
|
Loading…
Reference in New Issue
Block a user