mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-07 22:58:19 +08:00
commit
8af2792bb5
16
src/App.jsx
16
src/App.jsx
@ -25,14 +25,14 @@ function App() {
|
||||
|
||||
// 如果用户已认证但WebSocket未连接,则初始化连接
|
||||
if (user && !isConnected) {
|
||||
// initWebSocket()
|
||||
// .then(() => {
|
||||
// dispatch(setWebSocketConnected(true));
|
||||
// console.log('WebSocket connection initialized');
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error('Failed to initialize WebSocket connection:', error);
|
||||
// });
|
||||
initWebSocket()
|
||||
.then(() => {
|
||||
dispatch(setWebSocketConnected(true));
|
||||
console.log('WebSocket connection initialized');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to initialize WebSocket connection:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 组件卸载或用户登出时关闭WebSocket连接
|
||||
|
@ -15,6 +15,7 @@ import SvgIcon from './SvgIcon';
|
||||
* @param {boolean} props.isSearchLoading - 搜索是否正在加载
|
||||
* @param {Function} props.onResultClick - 点击搜索结果的回调
|
||||
* @param {Function} props.onRequestAccess - 申请权限的回调
|
||||
* @param {string} props.cornerStyle - 设置圆角风格,可选值: 'rounded'(圆角) 或 'square'(方角)
|
||||
*/
|
||||
const SearchBar = ({
|
||||
searchKeyword,
|
||||
@ -22,17 +23,23 @@ const SearchBar = ({
|
||||
onSearchChange,
|
||||
onSearch,
|
||||
onClearSearch,
|
||||
placeholder = '搜索...',
|
||||
placeholder = '搜索知识库...',
|
||||
className = 'w-50',
|
||||
searchResults = [],
|
||||
isSearchLoading = false,
|
||||
onResultClick,
|
||||
onRequestAccess,
|
||||
cornerStyle = 'rounded', // 默认为圆角
|
||||
}) => {
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const searchRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
// 计算边框圆角样式类
|
||||
const getBorderRadiusClass = () => {
|
||||
return cornerStyle === 'rounded' ? 'rounded-pill' : 'rounded-0';
|
||||
};
|
||||
|
||||
// 处理点击外部关闭下拉框
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
@ -69,11 +76,11 @@ const SearchBar = ({
|
||||
return (
|
||||
<div className={`position-relative ${className}`} ref={searchRef}>
|
||||
<form className='d-flex' onSubmit={handleSubmit}>
|
||||
<div className='input-group'>
|
||||
<div className={`input-group search-input-group ${getBorderRadiusClass()}`}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
className='form-control'
|
||||
className={`form-control search-input border-end-0 ${getBorderRadiusClass()}`}
|
||||
placeholder={placeholder}
|
||||
value={searchKeyword}
|
||||
onChange={handleInputChange}
|
||||
@ -81,7 +88,9 @@ const SearchBar = ({
|
||||
{searchKeyword.trim() && (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-outline-secondary border-start-0'
|
||||
className={`btn btn-outline-secondary border-start-0 ${
|
||||
cornerStyle === 'rounded' ? '' : 'rounded-0'
|
||||
}`}
|
||||
onClick={() => {
|
||||
onClearSearch();
|
||||
setShowDropdown(false);
|
||||
@ -91,7 +100,12 @@ const SearchBar = ({
|
||||
<SvgIcon className='close' />
|
||||
</button>
|
||||
)}
|
||||
<button type='submit' className='btn btn-outline-secondary'>
|
||||
<button
|
||||
type='submit'
|
||||
className={`btn btn-outline-secondary search-button ${
|
||||
cornerStyle === 'rounded' ? 'rounded-end-pill' : 'rounded-0'
|
||||
}`}
|
||||
>
|
||||
<SvgIcon className='search' />
|
||||
</button>
|
||||
</div>
|
||||
@ -99,7 +113,11 @@ const SearchBar = ({
|
||||
|
||||
{/* 搜索结果下拉框 - 仅在用户搜索且有结果时显示 */}
|
||||
{showDropdown && (isSearchLoading || searchResults?.length > 0) && (
|
||||
<div className='position-absolute bg-white shadow-sm rounded-3 mt-1 w-100 search-results-dropdown z-1'>
|
||||
<div
|
||||
className={`position-absolute bg-white shadow-sm mt-1 w-100 search-results-dropdown z-1 ${
|
||||
cornerStyle === 'rounded' ? 'rounded-3' : ''
|
||||
}`}
|
||||
>
|
||||
<div className='p-2 overflow-auto' style={{ maxHeight: '350px', zIndex: '1050' }}>
|
||||
{isSearchLoading ? (
|
||||
<div className='text-center p-3'>
|
||||
@ -116,7 +134,9 @@ const SearchBar = ({
|
||||
{searchResults.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className='search-result-item p-2 rounded-2 mb-1 hover-bg-light'
|
||||
className={`search-result-item p-2 mb-1 hover-bg-light ${
|
||||
cornerStyle === 'rounded' ? 'rounded-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
cursor: item.permissions?.can_read ? 'pointer' : 'default',
|
||||
}}
|
||||
@ -167,7 +187,9 @@ const SearchBar = ({
|
||||
</div>
|
||||
{!item.permissions?.can_read && (
|
||||
<button
|
||||
className='btn btn-sm btn-outline-primary ms-2'
|
||||
className={`btn btn-sm btn-outline-primary ms-2 ${
|
||||
cornerStyle === 'rounded' ? '' : 'rounded-0'
|
||||
}`}
|
||||
onClick={() => {
|
||||
onRequestAccess(item.id, item.name);
|
||||
setShowDropdown(false);
|
||||
|
@ -79,7 +79,7 @@ export default function HeaderWithNav() {
|
||||
</ul>
|
||||
{!!user ? (
|
||||
<div className='d-flex align-items-center gap-3'>
|
||||
{/* <div className='position-relative'>
|
||||
<div className='position-relative'>
|
||||
<button
|
||||
className='btn btn-link text-dark p-0'
|
||||
onClick={() => setShowNotifications(!showNotifications)}
|
||||
@ -102,7 +102,7 @@ export default function HeaderWithNav() {
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className='flex-shrink-0 dropdown'>
|
||||
<a
|
||||
href='#'
|
||||
|
@ -9,8 +9,10 @@ const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, '');
|
||||
let socket = null;
|
||||
let reconnectTimer = null;
|
||||
let pingInterval = null;
|
||||
let reconnectAttempts = 0; // 添加重连尝试计数器
|
||||
const RECONNECT_DELAY = 5000; // 5秒后尝试重连
|
||||
const PING_INTERVAL = 30000; // 30秒发送一次ping
|
||||
const MAX_RECONNECT_ATTEMPTS = 3; // 最大重连尝试次数
|
||||
|
||||
/**
|
||||
* 初始化WebSocket连接
|
||||
@ -37,11 +39,13 @@ export const initWebSocket = () => {
|
||||
}
|
||||
|
||||
const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${encryptedToken}`;
|
||||
console.log('WebSocket URL:', wsUrl);
|
||||
socket = new WebSocket(wsUrl);
|
||||
|
||||
// 连接建立时的处理
|
||||
socket.onopen = () => {
|
||||
console.log('WebSocket connection established');
|
||||
reconnectAttempts = 0; // 连接成功后重置重连计数器
|
||||
|
||||
// 订阅通知频道
|
||||
subscribeToNotifications();
|
||||
@ -81,12 +85,19 @@ export const initWebSocket = () => {
|
||||
|
||||
// 如果不是正常关闭,尝试重连
|
||||
if (event.code !== 1000) {
|
||||
reconnectTimer = setTimeout(() => {
|
||||
console.log('Attempting to reconnect WebSocket...');
|
||||
initWebSocket().catch((err) => {
|
||||
console.error('Failed to reconnect WebSocket:', err);
|
||||
});
|
||||
}, RECONNECT_DELAY);
|
||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||
reconnectAttempts++;
|
||||
console.log(`WebSocket reconnection attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`);
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
console.log('Attempting to reconnect WebSocket...');
|
||||
initWebSocket().catch((err) => {
|
||||
console.error('Failed to reconnect WebSocket:', err);
|
||||
});
|
||||
}, RECONNECT_DELAY);
|
||||
} else {
|
||||
console.log('Maximum reconnection attempts reached. Giving up.');
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
|
@ -18,17 +18,33 @@
|
||||
color: inherit;
|
||||
|
||||
/* Heading styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.5rem; }
|
||||
h2 { font-size: 1.35rem; }
|
||||
h3 { font-size: 1.2rem; }
|
||||
h4 { font-size: 1.1rem; }
|
||||
h5, h6 { font-size: 1rem; }
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Paragraph spacing */
|
||||
p {
|
||||
@ -36,19 +52,21 @@
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol {
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Code blocks with syntax highlighting */
|
||||
pre, pre.prism-code {
|
||||
pre,
|
||||
pre.prism-code {
|
||||
margin: 0.5rem 0 !important;
|
||||
padding: 0.75rem !important;
|
||||
border-radius: 0.375rem !important;
|
||||
font-size: 0.85rem !important;
|
||||
line-height: 1.5 !important;
|
||||
|
||||
|
||||
/* Improve readability on dark background */
|
||||
code span {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||
@ -91,12 +109,13 @@
|
||||
width: 100%;
|
||||
margin-bottom: 0.75rem;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
@ -120,20 +139,20 @@
|
||||
/* Apply different text colors based on message background */
|
||||
.bg-dark .markdown-content {
|
||||
color: white;
|
||||
|
||||
|
||||
code {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
|
||||
pre {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
border-left-color: rgba(255, 255, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: #8bb9fe;
|
||||
}
|
||||
@ -142,8 +161,8 @@
|
||||
.knowledge-card {
|
||||
min-width: 20rem;
|
||||
cursor: pointer;
|
||||
|
||||
.hoverdown:hover .hoverdown-menu{
|
||||
|
||||
.hoverdown:hover .hoverdown-menu {
|
||||
display: block;
|
||||
color: red;
|
||||
}
|
||||
@ -161,7 +180,7 @@
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
color: $dark;
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-100;
|
||||
}
|
||||
@ -178,41 +197,41 @@
|
||||
|
||||
/* 自定义黑色系开关样式 */
|
||||
.dark-switch .form-check-input {
|
||||
border: 1px solid #dee2e6;
|
||||
background-color: #fff; /* 关闭状态背景色 */
|
||||
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>");
|
||||
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;
|
||||
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>");
|
||||
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);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.dark-switch .form-check-input:disabled {
|
||||
opacity: 0.5;
|
||||
background-color: #e9ecef;
|
||||
opacity: 0.5;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
// 通知中心样式
|
||||
.notification-item {
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-100;
|
||||
}
|
||||
@ -230,48 +249,48 @@
|
||||
}
|
||||
|
||||
.dark-pagination .page-link {
|
||||
color: #000; /* 默认文字颜色 */
|
||||
color: #000; /* 默认文字颜色 */
|
||||
background-color: #fff; /* 默认背景 */
|
||||
border: 1px solid #dee2e6; /* 边框颜色 */
|
||||
transition: all 0.3s ease; /* 平滑过渡效果 */
|
||||
}
|
||||
|
||||
/* 激活状态 */
|
||||
.dark-pagination .page-item.active .page-link {
|
||||
}
|
||||
|
||||
/* 激活状态 */
|
||||
.dark-pagination .page-item.active .page-link {
|
||||
background-color: #000 !important;
|
||||
border-color: #000;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
/* 悬停状态 */
|
||||
.dark-pagination .page-link:hover {
|
||||
}
|
||||
|
||||
/* 悬停状态 */
|
||||
.dark-pagination .page-link:hover {
|
||||
background-color: #f8f9fa; /* 浅灰背景 */
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.dark-pagination .page-item.disabled .page-link {
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.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 {
|
||||
}
|
||||
|
||||
/* 自定义下拉框 */
|
||||
.dark-select {
|
||||
border: 1px solid #000 !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.dark-select:focus {
|
||||
}
|
||||
|
||||
.dark-select:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.25); /* 黑色聚焦阴影 */
|
||||
}
|
||||
|
||||
/* 下拉箭头颜色 */
|
||||
.dark-select {
|
||||
}
|
||||
|
||||
/* 下拉箭头颜色 */
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
/* Code Block Styles */
|
||||
.code-block-container {
|
||||
@ -310,7 +329,7 @@
|
||||
gap: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
@ -333,11 +352,11 @@
|
||||
/* Markdown fallback styling */
|
||||
.markdown-fallback {
|
||||
font-size: 0.95rem;
|
||||
|
||||
|
||||
.text-danger {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
@ -350,39 +369,77 @@
|
||||
|
||||
/* Streaming message indicator */
|
||||
.streaming-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 5px;
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #6c757d;
|
||||
border-radius: 50%;
|
||||
margin: 0 2px;
|
||||
animation: pulse 1.5s infinite ease-in-out;
|
||||
|
||||
&.dot1 {
|
||||
animation-delay: 0s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 5px;
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #6c757d;
|
||||
border-radius: 50%;
|
||||
margin: 0 2px;
|
||||
animation: pulse 1.5s infinite ease-in-out;
|
||||
|
||||
&.dot1 {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&.dot2 {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
&.dot3 {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
&.dot2 {
|
||||
animation-delay: 0.3s;
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.dot3 {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
// SearchBar component styles
|
||||
.search-input-group {
|
||||
border: 1px solid #ced4da;
|
||||
overflow: hidden;
|
||||
|
||||
&.rounded-pill {
|
||||
border-radius: 50rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 1;
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #6c757d;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: transparent;
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
@ -6,18 +6,20 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
|
||||
return {
|
||||
base: '/',
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: '../dist',
|
||||
outDir: 'dist',
|
||||
},
|
||||
server: {
|
||||
port: env.VITE_PORT,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_URL || 'http://81.69.223.133:58008',
|
||||
target: env.VITE_API_URL || 'http://81.69.223.133:8008',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
historyApiFallback: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user