[dev]register && create knowledgebase api test

This commit is contained in:
susie-laptop 2025-03-21 22:13:42 -04:00
parent 6f48ff656b
commit 167b06315d
18 changed files with 1297 additions and 362 deletions

6
package-lock.json generated
View File

@ -13,6 +13,7 @@
"axios": "^1.8.1",
"bootstrap": "^5.3.3",
"crypto-js": "^4.2.0",
"lodash": "^4.17.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
@ -3370,6 +3371,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

@ -15,6 +15,7 @@
"axios": "^1.8.1",
"bootstrap": "^5.3.3",
"crypto-js": "^4.2.0",
"lodash": "^4.17.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",

View File

@ -11,10 +11,48 @@ import SvgIcon from './SvgIcon';
* @param {Function} props.onClose - 关闭弹窗的回调函数
* @param {Function} props.onChange - 表单输入变化的回调函数
* @param {Function} props.onSubmit - 提交表单的回调函数
* @param {Object} props.currentUser - 当前用户信息
*/
const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => {
const CreateKnowledgeBaseModal = ({
show,
formData,
formErrors,
isSubmitting,
onClose,
onChange,
onSubmit,
currentUser
}) => {
if (!show) return null;
//
const isAdmin = currentUser?.role === 'admin';
const isLeader = currentUser?.role === 'leader';
//
const getAvailableTypes = () => {
if (isAdmin) {
return [
{ value: 'admin', label: 'Admin 级知识库' },
{ value: 'leader', label: 'Leader 级知识库' },
{ value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' },
{ value: 'secret', label: '保密知识库' }
];
} else if (isLeader) {
return [
{ value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' }
];
} else {
return [
{ value: 'private', label: '私有知识库' }
];
}
};
const availableTypes = getAvailableTypes();
return (
<div
className='modal-backdrop'
@ -82,36 +120,29 @@ const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, on
<label className='form-label'>
知识库类型 <span className='text-danger'>*</span>
</label>
<div className='d-flex gap-3'>
<div className='form-check'>
<input
className='form-check-input'
type='radio'
name='type'
id='typePrivate'
value='private'
checked={formData.type === 'private'}
onChange={onChange}
/>
<label className='form-check-label' htmlFor='typePrivate'>
私有知识库
</label>
</div>
<div className='form-check'>
<input
className='form-check-input'
type='radio'
name='type'
id='typePublic'
value='public'
checked={formData.type === 'public'}
onChange={onChange}
/>
<label className='form-check-label' htmlFor='typePublic'>
公共知识库
</label>
</div>
<div className='d-flex flex-wrap gap-3'>
{availableTypes.map((type, index) => (
<div className='form-check' key={index}>
<input
className='form-check-input'
type='radio'
name='type'
id={`type${type.value}`}
value={type.value}
checked={formData.type === type.value}
onChange={onChange}
/>
<label className='form-check-label' htmlFor={`type${type.value}`}>
{type.label}
</label>
</div>
))}
</div>
{!isAdmin && !isLeader && (
<small className='text-muted d-block mt-1'>
注意您当前只能创建私有知识库其他类型需要更高权限
</small>
)}
{formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>}
</div>
<div className='mb-3'>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef, useState, useEffect } from 'react';
import SvgIcon from './SvgIcon';
/**
@ -11,6 +11,10 @@ import SvgIcon from './SvgIcon';
* @param {Function} props.onClearSearch - 清除搜索的回调函数
* @param {string} props.placeholder - 搜索框占位文本
* @param {string} props.className - 额外的 CSS 类名
* @param {Array} props.searchResults - 搜索结果
* @param {boolean} props.isSearchLoading - 搜索是否正在加载
* @param {Function} props.onResultClick - 点击搜索结果的回调
* @param {Function} props.onRequestAccess - 申请权限的回调
*/
const SearchBar = ({
searchKeyword,
@ -20,22 +24,179 @@ const SearchBar = ({
onClearSearch,
placeholder = '搜索...',
className = 'w-50',
searchResults = [],
isSearchLoading = false,
onResultClick,
onRequestAccess,
}) => {
const [showDropdown, setShowDropdown] = useState(false);
const searchRef = useRef(null);
const inputRef = useRef(null);
//
useEffect(() => {
const handleClickOutside = (event) => {
if (searchRef.current && !searchRef.current.contains(event.target)) {
setShowDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
//
const handleFocus = () => {
if (searchKeyword.trim().length > 0) {
setShowDropdown(true);
}
};
//
const handleInputChange = (e) => {
const value = e.target.value;
onSearchChange(e);
if (value.trim().length > 0) {
setShowDropdown(true);
} else {
setShowDropdown(false);
}
};
//
const handleSubmit = (e) => {
e.preventDefault();
onSearch(e);
if (searchKeyword.trim().length > 0) {
setShowDropdown(true);
}
};
return (
<form className={`d-flex ${className}`} onSubmit={onSearch}>
<input
type='text'
className='form-control'
placeholder={placeholder}
value={searchKeyword}
onChange={onSearchChange}
/>
{isSearching && (
<button type='button' className='btn btn-outline-dark ms-2' onClick={onClearSearch}>
清除
</button>
<div className={`position-relative ${className}`} ref={searchRef}>
<form className='d-flex' onSubmit={handleSubmit}>
<div className='input-group'>
<input
ref={inputRef}
type='text'
className='form-control'
placeholder={placeholder}
value={searchKeyword}
onChange={handleInputChange}
onFocus={handleFocus}
/>
{searchKeyword.trim() && (
<button
type='button'
className='btn btn-outline-secondary border-start-0'
onClick={() => {
onClearSearch();
setShowDropdown(false);
inputRef.current?.focus();
}}
>
<SvgIcon className='close' />
</button>
)}
<button type='submit' className='btn btn-outline-secondary'>
<SvgIcon className='search' />
</button>
</div>
</form>
{/* 搜索结果下拉框 */}
{showDropdown && (
<div className='position-absolute bg-white shadow-sm rounded-3 mt-1 w-100 search-results-dropdown'>
<div className='p-2 overflow-auto' style={{ maxHeight: '350px', zIndex: '1050' }}>
{isSearchLoading ? (
<div className='text-center p-3'>
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
<span className='ms-2'>搜索中...</span>
</div>
) : searchResults?.length > 0 ? (
<>
<div className='fw-bold text-secondary px-2 mb-2'>
搜索结果 ({searchResults.length})
</div>
{searchResults.map((item) => (
<div
key={item.id}
className='search-result-item p-2 rounded-2 mb-1 hover-bg-light'
style={{
cursor: item.permissions?.can_read ? 'pointer' : 'default',
}}
>
<div className='d-flex justify-content-between align-items-center'>
<div
className='flex-grow-1'
onClick={() => {
if (item.permissions?.can_read) {
onResultClick(item.id, item.permissions);
setShowDropdown(false);
}
}}
>
<div className='d-flex align-items-center'>
<SvgIcon className='knowledge-base' />
<span className='ms-2 fw-medium'>
{item.highlighted_name ? (
<span
dangerouslySetInnerHTML={{
__html: item.highlighted_name,
}}
/>
) : (
item.name
)}
</span>
</div>
<div className='small text-secondary d-flex align-items-center mt-1'>
<span
className='badge me-2'
style={{
backgroundColor:
item.type === 'private' ? '#e9ecef' : '#cff4fc',
color: item.type === 'private' ? '#495057' : '#055160',
}}
>
{item.type === 'private' ? '私有' : item.type}
</span>
{item.department && <span className='me-2'>{item.department}</span>}
{!item.permissions?.can_read && (
<span className='text-danger'>
<SvgIcon className='lock' />
<span className='ms-1'>无权限</span>
</span>
)}
</div>
</div>
{!item.permissions?.can_read && (
<button
className='btn btn-sm btn-outline-primary ms-2'
onClick={() => {
onRequestAccess(item.id, item.name);
setShowDropdown(false);
}}
>
申请权限
</button>
)}
</div>
</div>
))}
</>
) : (
<div className='text-center text-secondary p-3'>未找到匹配的知识库</div>
)}
</div>
</div>
)}
</form>
</div>
);
};

View File

@ -58,9 +58,9 @@ export const icons = {
key: `<svg xmlns='http://www.w3.org/2000/svg' height='14' width='14' viewBox='0 0 512 512' fill='currentColor'>
<path d='M336 352c97.2 0 176-78.8 176-176S433.2 0 336 0S160 78.8 160 176c0 18.7 2.9 36.8 8.3 53.7L7 391c-4.5 4.5-7 10.6-7 17l0 80c0 13.3 10.7 24 24 24l80 0c13.3 0 24-10.7 24-24l0-40 40 0c13.3 0 24-10.7 24-24l0-40 40 0c6.4 0 12.5-2.5 17-7l33.3-33.3c16.9 5.4 35 8.3 53.7 8.3zM376 96a40 40 0 1 1 0 80 40 40 0 1 1 0-80z' />
</svg>`,
lock: `<svg xmlns='http://www.w3.org/2000/svg' height='14' width='12.25' viewBox='0 0 448 512' fill='currentColor'>
<path d='M144 144l0 48 160 0 0-48c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192l0-48C80 64.5 144.5 0 224 0s144 64.5 144 144l0 48 16 0c35.3 0 64 28.7 64 64l0 192c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 256c0-35.3 28.7-64 64-64l16 0z' />
</svg>`,
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>`,
'stack-fill': `<svg
t='1741043402869'
class='icon'
@ -103,5 +103,21 @@ export const icons = {
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>`,
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>`
'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>`,
close: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>`,
'knowledge-base': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5 10.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"/>
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"/>
</svg>`,
'knowledge-base-large': `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
<path d="M5 10.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"/>
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"/>
</svg>`,
plus: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>`,
};

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { showNotification } from '../../../store/notification.slice';
import { fetchKnowledgeBases } from '../../../store/knowledgeBase/knowledgeBase.thunks';
import { getKnowledgeBaseById } from '../../../store/knowledgeBase/knowledgeBase.thunks';
import SvgIcon from '../../../components/SvgIcon';
import DatasetTab from './DatasetTab';
import SettingsTab from './SettingsTab';
@ -14,16 +14,15 @@ export default function KnowledgeBaseDetail() {
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
// Get knowledge base details from Redux store
const { data, status } = useSelector((state) => state.knowledgeBase.list);
const knowledgeBase = data?.items?.find((kb) => kb.id === id);
const { data: knowledgeBase, status, error } = useSelector((state) => state.knowledgeBase.detail);
const isLoading = status === 'loading';
// Fetch knowledge bases if not available
// Fetch knowledge base details when component mounts or ID changes
useEffect(() => {
if (!data?.items?.length && status !== 'loading') {
dispatch(fetchKnowledgeBases());
if (id) {
dispatch(getKnowledgeBaseById(id));
}
}, [dispatch, data, status]);
}, [dispatch, id]);
// Update active tab when URL changes
useEffect(() => {
@ -32,18 +31,18 @@ export default function KnowledgeBaseDetail() {
}
}, [tab]);
// If knowledge base not found in Redux store, show notification and redirect
// If knowledge base not found, show notification and redirect
useEffect(() => {
if (!knowledgeBase && data?.items?.length > 0 && !isLoading) {
if (status === 'failed' && error) {
dispatch(
showNotification({
message: '未找到知识库,请返回知识库列表',
message: `获取知识库失败: ${error.message || '未找到知识库'}`,
type: 'warning',
})
);
navigate('/knowledge-base');
}
}, [knowledgeBase, data, isLoading, dispatch, navigate]);
}, [status, error, dispatch, navigate]);
// Handle tab change
const handleTabChange = (tab) => {
@ -69,9 +68,7 @@ export default function KnowledgeBaseDetail() {
<div className='col-md-3 col-lg-2 border-end'>
<div className='py-4'>
<div className='h4 mb-3 text-center'>{knowledgeBase.name}</div>
<p className='text-center text-muted small mb-4'>
{knowledgeBase.desc || ''}
</p>
<p className='text-center text-muted small mb-4'>{knowledgeBase.desc || ''}</p>
<hr />

View File

@ -1,8 +1,12 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { showNotification } from '../../../store/notification.slice';
import { updateKnowledgeBase, deleteKnowledgeBase } from '../../../store/knowledgeBase/knowledgeBase.thunks';
import {
updateKnowledgeBase,
deleteKnowledgeBase,
changeKnowledgeBaseType,
} from '../../../store/knowledgeBase/knowledgeBase.thunks';
//
import Breadcrumb from './components/Breadcrumb';
@ -10,25 +14,98 @@ import KnowledgeBaseForm from './components/KnowledgeBaseForm';
import DeleteConfirmModal from './components/DeleteConfirmModal';
import UserPermissionsManager from './components/UserPermissionsManager';
//
const departmentGroups = {
技术部: ['开发组', '测试组', '运维组', '架构组', '安全组'],
产品部: ['产品规划组', '用户研究组', '交互设计组', '项目管理组'],
市场部: ['品牌推广组', '市场调研组', '客户关系组', '社交媒体组'],
行政部: ['人事组', '财务组', '行政管理组', '后勤组'],
};
//
const departments = Object.keys(departmentGroups);
export default function SettingsTab({ knowledgeBase }) {
const dispatch = useDispatch();
const navigate = useNavigate();
const currentUser = useSelector((state) => state.auth.user);
const isAdmin = currentUser?.role === 'admin';
// State for knowledge base form
const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({
name: knowledgeBase.name,
desc: knowledgeBase.desc || knowledgeBase.description || '',
type: knowledgeBase.type || 'private', //
original_type: knowledgeBase.type || 'private', //
department: knowledgeBase.department || '',
group: knowledgeBase.group || '',
original_department: knowledgeBase.department || '',
original_group: knowledgeBase.group || '',
});
const [formErrors, setFormErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [availableGroups, setAvailableGroups] = useState([]);
//
useEffect(() => {
if (knowledgeBaseForm.department && departmentGroups[knowledgeBaseForm.department]) {
setAvailableGroups(departmentGroups[knowledgeBaseForm.department]);
//
if (!departmentGroups[knowledgeBaseForm.department].includes(knowledgeBaseForm.group)) {
setKnowledgeBaseForm((prev) => ({
...prev,
group: '',
}));
}
} else {
setAvailableGroups([]);
setKnowledgeBaseForm((prev) => ({
...prev,
group: '',
}));
}
}, [knowledgeBaseForm.department]);
//
useEffect(() => {
if (knowledgeBase.department && departmentGroups[knowledgeBase.department]) {
setAvailableGroups(departmentGroups[knowledgeBase.department]);
}
}, [knowledgeBase]);
// Handle knowledge base form input change
const handleInputChange = (e) => {
const { name, value } = e.target;
//
if (name === 'type') {
const role = currentUser?.role;
let allowed = false;
//
if (role === 'admin') {
//
allowed = ['admin', 'leader', 'member', 'private', 'secret'].includes(value);
} else if (role === 'leader') {
// member private
allowed = ['member', 'private'].includes(value);
} else {
// private
allowed = value === 'private';
}
if (!allowed) {
dispatch(
showNotification({
message: '您没有权限设置此类型的知识库',
type: 'warning',
})
);
return;
}
}
setKnowledgeBaseForm((prev) => ({
...prev,
[name]: value,
@ -59,10 +136,87 @@ export default function SettingsTab({ knowledgeBase }) {
errors.type = '请选择知识库类型';
}
if (isAdmin && !knowledgeBaseForm.department) {
errors.department = '请选择部门';
}
if (isAdmin && !knowledgeBaseForm.group) {
errors.group = '请选择组别';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
//
const hasDepartmentOrGroupChanged = () => {
return (
knowledgeBaseForm.department !== knowledgeBaseForm.original_department ||
knowledgeBaseForm.group !== knowledgeBaseForm.original_group
);
};
//
const handleTypeChange = (newType) => {
if (!currentUser) {
dispatch(
showNotification({
message: '用户信息不完整,无法更改类型',
type: 'warning',
})
);
return;
}
if (isAdmin && !validateForm()) {
return;
}
setIsSubmitting(true);
const department = isAdmin ? knowledgeBaseForm.department : currentUser.department || '';
const group = isAdmin ? knowledgeBaseForm.group : currentUser.group || '';
dispatch(
changeKnowledgeBaseType({
id: knowledgeBase.id,
type: newType,
department,
group,
})
)
.unwrap()
.then((updatedKB) => {
//
setKnowledgeBaseForm((prev) => ({
...prev,
type: updatedKB.type,
original_type: updatedKB.type,
department: updatedKB.department,
original_department: updatedKB.department,
group: updatedKB.group,
original_group: updatedKB.group,
}));
dispatch(
showNotification({
message: `知识库类型已更新为 ${updatedKB.type}`,
type: 'success',
})
);
setIsSubmitting(false);
})
.catch((error) => {
dispatch(
showNotification({
message: `类型更新失败: ${error.message || '未知错误'}`,
type: 'danger',
})
);
setIsSubmitting(false);
});
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
@ -74,7 +228,13 @@ export default function SettingsTab({ knowledgeBase }) {
setIsSubmitting(true);
// Dispatch update knowledge base action
// 使API
if (knowledgeBaseForm.type !== knowledgeBaseForm.original_type || (isAdmin && hasDepartmentOrGroupChanged())) {
handleTypeChange(knowledgeBaseForm.type);
return;
}
// Dispatch update knowledge base action ()
dispatch(
updateKnowledgeBase({
id: knowledgeBase.id,
@ -82,9 +242,6 @@ export default function SettingsTab({ knowledgeBase }) {
name: knowledgeBaseForm.name,
desc: knowledgeBaseForm.desc,
description: knowledgeBaseForm.desc, // Add description field for compatibility
type: knowledgeBaseForm.type,
department: knowledgeBaseForm.department,
group: knowledgeBaseForm.group,
},
})
)
@ -151,6 +308,10 @@ export default function SettingsTab({ knowledgeBase }) {
onInputChange={handleInputChange}
onSubmit={handleSubmit}
onDelete={() => setShowDeleteConfirm(true)}
onTypeChange={handleTypeChange}
isAdmin={isAdmin}
departments={departments}
availableGroups={availableGroups}
/>
{/* User Permissions Manager */}

View File

@ -1,4 +1,4 @@
import React, { useRef } from 'react';
import React, { useRef, useEffect } from 'react';
/**
* 文件上传模态框组件
@ -17,11 +17,44 @@ const FileUploadModal = ({
onUpload,
}) => {
const fileInputRef = useRef(null);
const modalRef = useRef(null);
//
const handleUploadAreaClick = () => {
fileInputRef.current?.click();
};
//
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
onDragOver?.(e);
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
onFileDrop?.(e);
};
//
useEffect(() => {
return () => {
//
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
if (modalRef.current) {
modalRef.current = null;
}
};
}, []);
if (!show) return null;
return (
<div
ref={modalRef}
className='modal-backdrop'
style={{
position: 'fixed',
@ -54,11 +87,17 @@ const FileUploadModal = ({
fileErrors.file ? 'border-danger' : 'border-dashed'
}`}
style={{ cursor: 'pointer' }}
onClick={onUploadAreaClick}
onDrop={onFileDrop}
onDragOver={onDragOver}
onClick={handleUploadAreaClick}
onDrop={handleDrop}
onDragOver={handleDragOver}
>
<input type='file' ref={fileInputRef} className='d-none' onChange={onFileChange} />
<input
type='file'
ref={fileInputRef}
className='d-none'
onChange={onFileChange}
accept='.pdf,.docx,.txt,.csv'
/>
{newFile.file ? (
<div>
<p className='mb-1'>已选择文件</p>

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
/**
* 知识库表单组件
@ -10,7 +11,50 @@ const KnowledgeBaseForm = ({
onInputChange,
onSubmit,
onDelete,
onTypeChange,
isAdmin,
departments,
availableGroups,
}) => {
//
const currentUser = useSelector((state) => state.auth.user);
//
const isLeader = currentUser?.role === 'leader';
//
const getAvailableTypes = () => {
if (isAdmin) {
return [
{ value: 'admin', label: 'Admin 级知识库' },
{ value: 'leader', label: 'Leader 级知识库' },
{ value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' },
{ value: 'secret', label: '保密知识库' },
];
} else if (isLeader) {
return [
{ value: 'member', label: 'Member 级知识库' },
{ value: 'private', label: '私有知识库' },
];
} else {
return [{ value: 'private', label: '私有知识库' }];
}
};
const availableTypes = getAvailableTypes();
//
const hasTypeChanged = formData.original_type && formData.original_type !== formData.type;
//
const hasDepartmentOrGroupChanged =
(formData.original_department && formData.department !== formData.original_department) ||
(formData.original_group && formData.group !== formData.original_group);
//
const showTypeChangeButton = hasTypeChanged || (isAdmin && hasDepartmentOrGroupChanged);
return (
<div className='card border-0 shadow-sm'>
<div className='card-body'>
@ -47,69 +91,151 @@ const KnowledgeBaseForm = ({
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
</div>
<div className='mb-3'>
<label className='form-label'>知识库类型</label>
<div className='form-check'>
<input
className='form-check-input'
type='radio'
name='type'
id='typePrivate'
value='private'
checked={formData.type === 'private'}
onChange={onInputChange}
/>
<label className='form-check-label' htmlFor='typePrivate'>
私有知识库
</label>
</div>
<div className='form-check'>
<input
className='form-check-input'
type='radio'
name='type'
id='typePublic'
value='public'
checked={formData.type === 'public'}
onChange={onInputChange}
/>
<label className='form-check-label' htmlFor='typePublic'>
公共知识库
</label>
<div className='mb-4'>
<label className='form-label'>
知识库类型 <span className='text-danger'>*</span>
</label>
<div className='d-flex flex-column gap-2'>
{availableTypes.map((type) => (
<div className='form-check' key={type.value}>
<input
className='form-check-input'
type='radio'
name='type'
id={`type${type.value}`}
value={type.value}
checked={formData.type === type.value}
onChange={onInputChange}
disabled={currentUser?.role === 'member'} //
/>
<label className='form-check-label' htmlFor={`type${type.value}`}>
{type.label}
</label>
</div>
))}
</div>
{currentUser?.role === 'member' && (
<small className='text-muted d-block mt-1'>您当前无权修改知识库类型</small>
)}
{formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>}
</div>
<div className='mb-3'>
<label htmlFor='department' className='form-label'>
部门
部门 {isAdmin && <span className='text-danger'>*</span>}
</label>
<input
type='text'
className='form-control bg-light'
id='department'
name='department'
value={formData.department || ''}
readOnly
/>
<small className='text-muted'>部门信息根据知识库创建者自动填写</small>
{isAdmin ? (
<>
<select
className={`form-select ${formErrors.department ? 'is-invalid' : ''}`}
id='department'
name='department'
value={formData.department || ''}
onChange={onInputChange}
disabled={isSubmitting}
>
<option value=''>请选择部门</option>
{departments.map((dept, index) => (
<option key={index} value={dept}>
{dept}
</option>
))}
</select>
{formErrors.department && (
<div className='invalid-feedback'>{formErrors.department}</div>
)}
</>
) : (
<>
<input
type='text'
className='form-control bg-light'
id='department'
name='department'
value={formData.department || ''}
readOnly
/>
<small className='text-muted'>部门信息根据知识库创建者自动填写</small>
</>
)}
</div>
<div className='mb-3'>
<label htmlFor='group' className='form-label'>
组别
组别 {isAdmin && <span className='text-danger'>*</span>}
</label>
<input
type='text'
className='form-control bg-light'
id='group'
name='group'
value={formData.group || ''}
readOnly
/>
<small className='text-muted'>组别信息根据知识库创建者自动填写</small>
{isAdmin ? (
<>
<select
className={`form-select ${formErrors.group ? 'is-invalid' : ''}`}
id='group'
name='group'
value={formData.group || ''}
onChange={onInputChange}
disabled={isSubmitting || !formData.department}
>
<option value=''>{formData.department ? '请选择组别' : '请先选择部门'}</option>
{availableGroups.map((group, index) => (
<option key={index} value={group}>
{group}
</option>
))}
</select>
{formErrors.group && <div className='invalid-feedback'>{formErrors.group}</div>}
{!formData.department && (
<small className='text-muted d-block mt-1'>请先选择部门</small>
)}
</>
) : (
<>
<input
type='text'
className='form-control bg-light'
id='group'
name='group'
value={formData.group || ''}
readOnly
/>
<small className='text-muted'>组别信息根据知识库创建者自动填写</small>
</>
)}
</div>
<div className='d-flex justify-content-between'>
{/* 类型更改按钮 */}
{showTypeChangeButton && (
<div className='alert alert-warning d-flex align-items-center justify-content-between'>
<div>
{hasTypeChanged && (
<p className='mb-0'>
知识库类型已更改为 <strong>{formData.type}</strong>
</p>
)}
{isAdmin && hasDepartmentOrGroupChanged && <p className='mb-0'>部门/组别已更改</p>}
<small>点击更新按钮单独保存这些更改</small>
</div>
<button
type='button'
className='btn btn-warning'
onClick={() => onTypeChange && onTypeChange(formData.type)}
disabled={isSubmitting || currentUser?.role === 'member'}
>
{isSubmitting ? (
<>
<span
className='spinner-border spinner-border-sm me-2'
role='status'
aria-hidden='true'
></span>
更新中...
</>
) : (
'更新知识库类型'
)}
</button>
</div>
)}
<div className='d-flex justify-content-between mt-4'>
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
{isSubmitting ? (
<>

View File

@ -0,0 +1,53 @@
.knowledge-base-page {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.knowledge-base-header {
background-color: #f8f9fa;
border-radius: 10px;
margin-bottom: 20px;
}
.knowledge-base-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.search-bar-container {
position: relative;
flex: 1;
max-width: 500px;
}
.search-results-dropdown {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
z-index: 1050;
}
.search-result-item {
transition: background-color 0.2s;
}
.search-result-item:hover {
background-color: #f8f9fa;
}
.hover-bg-light:hover {
background-color: #f8f9fa;
}
/* 响应式样式 */
@media (max-width: 768px) {
.knowledge-base-cards-container {
grid-template-columns: 1fr;
}
.search-bar-container {
max-width: 100%;
}
}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { showNotification } from '../../store/notification.slice';
@ -6,14 +6,17 @@ import {
fetchKnowledgeBases,
searchKnowledgeBases,
createKnowledgeBase,
deleteKnowledgeBase,
requestKnowledgeBaseAccess,
} from '../../store/knowledgeBase/knowledgeBase.thunks';
import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice';
import { clearSearchResults } from '../../store/knowledgeBase/knowledgeBase.slice';
import SvgIcon from '../../components/SvgIcon';
import { requestKnowledgeBaseAccess } from '../../services/permissionService';
import AccessRequestModal from '../../components/AccessRequestModal';
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
import Pagination from '../../components/Pagination';
import SearchBar from '../../components/SearchBar';
import ApiModeSwitch from '../../components/ApiModeSwitch';
import debounce from 'lodash/debounce';
//
import KnowledgeBaseList from './components/KnowledgeBaseList';
@ -29,6 +32,7 @@ export default function KnowledgeBase() {
title: '',
});
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
const [createdKnowledgeBaseId, setCreatedKnowledgeBaseId] = useState(null);
//
const currentUser = useSelector((state) => state.auth.user);
@ -52,20 +56,23 @@ export default function KnowledgeBase() {
});
// Get knowledge bases from Redux store
const { data, status, error } = useSelector((state) => state.knowledgeBase.list);
const {
data: searchData,
status: searchStatus,
error: searchError,
keyword: storeKeyword,
} = useSelector((state) => state.knowledgeBase.search);
const { status: operationStatus, error: operationError } = useSelector((state) => state.knowledgeBase.operations);
// Redux
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases);
const loading = useSelector((state) => state.knowledgeBase.loading);
const paginationData = useSelector((state) => state.knowledgeBase.pagination);
const error = useSelector((state) => state.knowledgeBase.error);
const operationStatus = useSelector((state) => state.knowledgeBase.editStatus);
const operationError = useSelector((state) => state.knowledgeBase.error);
// Redux
const searchResults = useSelector((state) => state.knowledgeBase.searchResults);
const searchLoading = useSelector((state) => state.knowledgeBase.searchLoading);
// Determine which data to display based on search state
const displayData = isSearching ? searchData?.items : data?.items;
const displayTotal = isSearching ? searchData?.total : data?.total;
const displayStatus = isSearching ? searchStatus : status;
const displayError = isSearching ? searchError : error;
const displayData = isSearching ? searchResults : knowledgeBases;
const displayTotal = paginationData.total;
const displayStatus = loading ? 'loading' : 'succeeded';
const displayError = error;
// Fetch knowledge bases when component mounts or pagination changes
useEffect(() => {
@ -82,9 +89,35 @@ export default function KnowledgeBase() {
}
}, [dispatch, pagination.page, pagination.page_size, isSearching, searchKeyword]);
//
const debouncedSearch = useCallback(
debounce((keyword) => {
if (keyword.trim()) {
dispatch(
searchKnowledgeBases({
keyword,
page: 1,
page_size: 5,
})
);
} else {
dispatch(clearSearchResults());
}
}, 300),
[dispatch]
);
// Handle search input change
const handleSearchInputChange = (e) => {
setSearchKeyword(e.target.value);
const value = e.target.value;
setSearchKeyword(value);
//
if (value.trim()) {
debouncedSearch(value);
} else {
dispatch(clearSearchResults());
}
};
// Handle search submit
@ -112,7 +145,7 @@ export default function KnowledgeBase() {
setSearchKeyword('');
setIsSearching(false);
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
dispatch(resetSearchState());
dispatch(clearSearchResults());
};
// Show loading state while fetching data
@ -132,13 +165,9 @@ export default function KnowledgeBase() {
// Show notification for operation status
useEffect(() => {
if (operationStatus === 'succeeded') {
dispatch(
showNotification({
message: '操作成功',
type: 'success',
})
);
if (operationStatus === 'successful') {
//
// Refresh the list after successful operation
if (isSearching && searchKeyword.trim()) {
dispatch(
@ -184,6 +213,34 @@ export default function KnowledgeBase() {
return;
}
//
if (name === 'type') {
const role = currentUser?.role;
let allowed = false;
//
if (role === 'admin') {
//
allowed = ['admin', 'leader', 'member', 'private', 'secret'].includes(value);
} else if (role === 'leader') {
// member private
allowed = ['member', 'private'].includes(value);
} else {
// private
allowed = value === 'private';
}
if (!allowed) {
dispatch(
showNotification({
message: '您没有权限创建此类型的知识库',
type: 'warning',
})
);
return;
}
}
setNewKnowledgeBase((prev) => ({
...prev,
[name]: value,
@ -217,23 +274,73 @@ export default function KnowledgeBase() {
return Object.keys(errors).length === 0;
};
const handleCreateKnowledgeBase = () => {
const handleCreateKnowledgeBase = async () => {
// Validate form
if (!validateCreateForm()) {
return;
}
// Dispatch create knowledge base action
dispatch(
createKnowledgeBase({
name: newKnowledgeBase.name,
desc: newKnowledgeBase.desc,
description: newKnowledgeBase.desc,
type: newKnowledgeBase.type,
department: newKnowledgeBase.department,
group: newKnowledgeBase.group,
})
);
try {
// Dispatch create knowledge base action
const resultAction = await dispatch(
createKnowledgeBase({
name: newKnowledgeBase.name,
desc: newKnowledgeBase.desc,
description: newKnowledgeBase.desc,
type: newKnowledgeBase.type,
department: newKnowledgeBase.department,
group: newKnowledgeBase.group,
})
);
console.log('创建知识库返回数据:', resultAction);
// Check if the action was successful
if (createKnowledgeBase.fulfilled.match(resultAction)) {
console.log('创建成功payload:', resultAction.payload);
const { knowledge_base } = resultAction.payload;
const { id } = knowledge_base;
// Get ID from payload and navigate
if (id) {
console.log('新知识库ID:', id);
//
dispatch(
showNotification({
message: '知识库创建成功',
type: 'success',
})
);
//
navigate(`/knowledge-base/${id}`);
} else {
console.error('无法获取新知识库ID:', resultAction.payload);
dispatch(
showNotification({
message: '创建成功但无法获取知识库ID',
type: 'warning',
})
);
}
} else {
console.error('创建知识库失败:', resultAction.error);
dispatch(
showNotification({
message: `创建知识库失败: ${resultAction.error?.message || '未知错误'}`,
type: 'danger',
})
);
}
} catch (error) {
console.error('创建知识库出错:', error);
dispatch(
showNotification({
message: `创建知识库出错: ${error.message || '未知错误'}`,
type: 'danger',
})
);
}
// Reset form and close modal
setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
@ -297,7 +404,25 @@ export default function KnowledgeBase() {
const handleDelete = (e, id) => {
e.preventDefault();
e.stopPropagation();
console.log(id);
// Dispatch delete knowledge base action
dispatch(deleteKnowledgeBase(id))
.unwrap()
.then(() => {
dispatch(
showNotification({
message: '知识库已删除',
type: 'success',
})
);
})
.catch((error) => {
dispatch(
showNotification({
message: `删除失败: ${error.message || '未知错误'}`,
type: 'danger',
})
);
});
};
// Calculate total pages
@ -305,17 +430,31 @@ export default function KnowledgeBase() {
//
const handleOpenCreateModal = () => {
//
let defaultType = 'private';
// 使
setNewKnowledgeBase((prev) => ({
...prev,
department: currentUser?.department || '',
group: currentUser?.group || '',
type: defaultType,
}));
setShowCreateModal(true);
};
//
const handleSearchResultClick = (id, permissions) => {
if (permissions?.can_read) {
navigate(`/knowledge-base/${id}/datasets`);
}
};
return (
<div className='knowledge-base container my-4'>
<div className='api-mode-control mb-3'>
<ApiModeSwitch />
</div>
<div className='d-flex justify-content-between align-items-center mb-3'>
<SearchBar
searchKeyword={searchKeyword}
@ -324,6 +463,10 @@ export default function KnowledgeBase() {
onSearch={handleSearch}
onClearSearch={handleClearSearch}
placeholder='搜索知识库...'
searchResults={searchResults}
isSearchLoading={searchLoading}
onResultClick={handleSearchResultClick}
onRequestAccess={handleRequestAccess}
/>
<button className='btn btn-dark d-flex align-items-center gap-1' onClick={handleOpenCreateModal}>
<SvgIcon className={'plus'} />
@ -333,7 +476,7 @@ export default function KnowledgeBase() {
{isSearching && (
<div className='alert alert-info'>
搜索结果: "{storeKeyword}" - 找到 {displayTotal} 个知识库
搜索结果: "{searchKeyword}" - 找到 {displayTotal} 个知识库
</div>
)}
@ -371,10 +514,11 @@ export default function KnowledgeBase() {
show={showCreateModal}
formData={newKnowledgeBase}
formErrors={formErrors}
isSubmitting={operationStatus === 'loading'}
isSubmitting={loading}
onClose={() => setShowCreateModal(false)}
onChange={handleInputChange}
onSubmit={handleCreateKnowledgeBase}
currentUser={currentUser}
/>
{/* 申请权限弹窗 */}

View File

@ -3,7 +3,6 @@ import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import PendingRequests from './components/PendingRequests';
import UserPermissions from './components/UserPermissions';
import ApiModeSwitch from '../../components/ApiModeSwitch';
import './Permissions.css';
export default function PermissionsPage() {
@ -18,11 +17,7 @@ export default function PermissionsPage() {
}, [user, navigate]);
return (
<div className='permissions-container'>
{/* <div className='api-mode-control mb-3'>
<ApiModeSwitch />
</div> */}
<div className='permissions-container'>
<div className='permissions-section mb-4'>
<PendingRequests />
</div>

View File

@ -3,21 +3,56 @@ import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { checkAuthThunk, signupThunk } from '../../store/auth/auth.thunk';
//
const departmentGroups = {
'技术部': ['开发组', '测试组', '运维组'],
'产品部': ['产品规划组', '用户研究组', '交互设计组', '项目管理组'],
'市场部': ['品牌推广组', '市场调研组', '客户关系组', '社交媒体组'],
'行政部': ['人事组', '财务组', '行政管理组', '后勤组']
};
export default function Signup() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
name: '',
role: 'member',
department: '',
group: '',
});
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
const [availableGroups, setAvailableGroups] = useState([]);
const { user } = useSelector((state) => state.auth);
const { user, loading } = useSelector((state) => state.auth);
useEffect(() => {
handleCheckAuth();
}, [dispatch]);
//
useEffect(() => {
if (formData.department && departmentGroups[formData.department]) {
setAvailableGroups(departmentGroups[formData.department]);
//
if (!departmentGroups[formData.department].includes(formData.group)) {
setFormData(prev => ({
...prev,
group: ''
}));
}
} else {
setAvailableGroups([]);
setFormData(prev => ({
...prev,
group: ''
}));
}
}, [formData.department]);
const handleCheckAuth = async () => {
console.log('signup page handleCheckAuth');
try {
@ -26,23 +61,52 @@ export default function Signup() {
} catch (error) {}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
//
if (errors[name]) {
setErrors({
...errors,
[name]: '',
});
}
};
const validateForm = () => {
const newErrors = {};
if (!username) {
if (!formData.username) {
newErrors.username = 'Username is required';
}
if (!email) {
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email)) {
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formData.email)) {
newErrors.email = 'Invalid email address';
}
if (!password) {
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (password.length < 6) {
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
if (!formData.name) {
newErrors.name = 'Name is required';
}
if (!formData.department) {
newErrors.department = '请选择部门';
}
if (!formData.group) {
newErrors.group = '请选择组别';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@ -50,16 +114,13 @@ export default function Signup() {
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitted(true);
console.log(validateForm());
if (validateForm()) {
console.log('Form submitted successfully!');
console.log('Username:', username);
console.log('Email:', email);
console.log('Password:', password);
console.log('Registration data:', formData);
try {
await dispatch(signupThunk({ username, password, email })).unwrap();
navigate('/');
await dispatch(signupThunk(formData)).unwrap();
navigate('/login');
} catch (error) {
console.error('Signup failed:', error);
}
@ -79,9 +140,12 @@ export default function Signup() {
type='text'
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
id='username'
placeholder='Username'
name='username'
placeholder='用户名'
value={formData.username}
required
onChange={(e) => setUsername(e.target.value.trim())}
onChange={handleInputChange}
disabled={loading}
></input>
{submitted && errors.username && <div className='invalid-feedback'>{errors.username}</div>}
</div>
@ -90,9 +154,12 @@ export default function Signup() {
type='email'
className={`form-control form-control-lg${submitted && errors.email ? ' is-invalid' : ''}`}
id='email'
placeholder='Email'
name='email'
placeholder='邮箱'
value={formData.email}
required
onChange={(e) => setEmail(e.target.value.trim())}
onChange={handleInputChange}
disabled={loading}
></input>
{submitted && errors.email && <div className='invalid-feedback'>{errors.email}</div>}
</div>
@ -100,20 +167,103 @@ export default function Signup() {
<input
type='password'
id='password'
placeholder='Password'
name='password'
placeholder='密码'
value={formData.password}
required
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
aria-describedby='passwordHelpBlock'
onChange={(e) => setPassword(e.target.value.trim())}
onChange={handleInputChange}
disabled={loading}
></input>
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
</div>
<button type='submit' className='btn btn-dark btn-lg w-100'>
Sign Up
<div className='input-group has-validation'>
<input
type='text'
className={`form-control form-control-lg${submitted && errors.name ? ' is-invalid' : ''}`}
id='name'
name='name'
placeholder='姓名'
value={formData.name}
required
onChange={handleInputChange}
disabled={loading}
></input>
{submitted && errors.name && <div className='invalid-feedback'>{errors.name}</div>}
</div>
<div className='input-group has-validation'>
<select
className={`form-select form-select-lg${submitted && errors.department ? ' is-invalid' : ''}`}
id='department'
name='department'
value={formData.department}
onChange={handleInputChange}
disabled={loading}
required
>
<option value='' disabled>
选择部门
</option>
<option value='技术部'>技术部</option>
<option value='产品部'>产品部</option>
<option value='市场部'>市场部</option>
<option value='行政部'>行政部</option>
</select>
{submitted && errors.department && <div className='invalid-feedback'>{errors.department}</div>}
</div>
<div className='input-group has-validation'>
<select
className={`form-select form-select-lg${submitted && errors.group ? ' is-invalid' : ''}`}
id='group'
name='group'
value={formData.group}
onChange={handleInputChange}
disabled={loading || !formData.department}
required
>
<option value='' disabled>
{formData.department ? '选择组别' : '请先选择部门'}
</option>
{availableGroups.map((group, index) => (
<option key={index} value={group}>
{group}
</option>
))}
</select>
{submitted && errors.group && <div className='invalid-feedback'>{errors.group}</div>}
</div>
<div className='input-group'>
<select
className='form-select form-select-lg'
id='role'
name='role'
value={formData.role}
onChange={handleInputChange}
disabled={loading}
>
<option value='member'>普通成员</option>
<option value='leader'>组长</option>
<option value='admin'>管理员</option>
</select>
</div>
<button type='submit' className='btn btn-dark btn-lg w-100' disabled={loading}>
{loading ? (
<>
<span
className='spinner-border spinner-border-sm me-2'
role='status'
aria-hidden='true'
></span>
注册中...
</>
) : (
'注册'
)}
</button>
</form>
<Link to='/login' className='go-to-signup w-100 link-underline-light h5 text-center'>
Already have account?
已有账号立即登录
</Link>
</div>
);

View File

@ -77,7 +77,7 @@ api.interceptors.response.use(
// 检查服务器状态
export const checkServerStatus = async () => {
try {
await api.get('/health-check', { timeout: 3000 });
// await api.get('/health-check', { timeout: 3000 });
isServerDown = false;
hasCheckedServer = true;
console.log('Server connection established');

View File

@ -35,15 +35,35 @@ export const loginThunk = createAsyncThunk(
}
);
export const signupThunk = createAsyncThunk('auth/signup', async (config, { rejectWithValue, dispatch }) => {
export const signupThunk = createAsyncThunk('auth/signup', async (userData, { rejectWithValue, dispatch }) => {
try {
const { message, user } = await post('/signup', config);
if (!user) {
throw new Error(message || 'Something went wrong');
// 使用新的注册 API
const { data, code } = await post('/auth/register/', userData);
console.log('注册返回数据:', response);
// 处理新的返回格式
if (code === 200) {
// // 将 token 加密存储到 sessionStorage
// const { token } = data;
// if (token) {
// const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
// sessionStorage.setItem('token', encryptedToken);
// }
// 显示注册成功通知
dispatch(
showNotification({
message: '注册成功',
type: 'success',
})
);
return null;
// return userData;
}
return user;
return response.data;
} catch (error) {
const errorMessage = error.response?.data?.message || 'Signup failed. Please try again.';
const errorMessage = error.response?.data?.message || '注册失败,请稍后重试';
dispatch(
showNotification({
message: errorMessage,

View File

@ -1,198 +1,169 @@
import { createSlice } from '@reduxjs/toolkit';
import {
fetchKnowledgeBases,
searchKnowledgeBases,
createKnowledgeBase,
getKnowledgeBaseById,
updateKnowledgeBase,
deleteKnowledgeBase,
changeKnowledgeBaseType,
searchKnowledgeBases,
requestKnowledgeBaseAccess,
} from './knowledgeBase.thunks';
const initialState = {
// List state
list: {
data: {
items: [],
total: 0,
page: 1,
page_size: 10,
},
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
// Search state
search: {
data: {
items: [],
total: 0,
page: 1,
page_size: 10,
keyword: '',
},
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
// Current knowledge base details
current: {
data: null,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
// Create/update/delete operations status
operations: {
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
operationType: null, // 'create' | 'update' | 'delete'
knowledgeBases: [],
currentKnowledgeBase: null,
searchResults: [],
searchLoading: false,
loading: false,
error: null,
pagination: {
total: 0,
page: 1,
page_size: 10,
total_pages: 1,
},
batchPermissions: {},
batchLoading: false,
editStatus: 'idle',
requestAccessStatus: 'idle',
};
const knowledgeBaseSlice = createSlice({
name: 'knowledgeBase',
initialState,
reducers: {
resetOperationStatus: (state) => {
state.operations = {
status: 'idle',
error: null,
operationType: null,
};
clearCurrentKnowledgeBase: (state) => {
state.currentKnowledgeBase = null;
},
resetCurrentKnowledgeBase: (state) => {
state.current = {
data: null,
status: 'idle',
error: null,
};
clearSearchResults: (state) => {
state.searchResults = [];
},
resetSearchState: (state) => {
state.search = {
data: {
items: [],
total: 0,
page: 1,
page_size: 10,
keyword: '',
},
status: 'idle',
error: null,
};
clearEditStatus: (state) => {
state.editStatus = 'idle';
},
},
extraReducers: (builder) => {
builder
// Fetch knowledge bases
// 获取知识库列表
.addCase(fetchKnowledgeBases.pending, (state) => {
state.list.status = 'loading';
state.loading = true;
state.error = null;
})
.addCase(fetchKnowledgeBases.fulfilled, (state, action) => {
state.list.status = 'succeeded';
state.list.data = action.payload;
state.list.error = null;
state.loading = false;
state.knowledgeBases = action.payload.items || [];
state.pagination = {
total: action.payload.total || 0,
page: action.payload.page || 1,
page_size: action.payload.page_size || 10,
total_pages: action.payload.total_pages || 1,
};
})
.addCase(fetchKnowledgeBases.rejected, (state, action) => {
state.list.status = 'failed';
state.list.error = action.payload;
state.loading = false;
state.error = action.payload || 'Failed to fetch knowledge bases';
})
// Search knowledge bases
.addCase(searchKnowledgeBases.pending, (state, action) => {
state.search.status = 'loading';
// Store the keyword for reference
if (action.meta.arg.keyword) {
state.search.data.keyword = action.meta.arg.keyword;
}
})
.addCase(searchKnowledgeBases.fulfilled, (state, action) => {
state.search.status = 'succeeded';
state.search.data = action.payload;
state.search.error = null;
})
.addCase(searchKnowledgeBases.rejected, (state, action) => {
state.search.status = 'failed';
state.search.error = action.payload;
})
// Get knowledge base by ID
.addCase(getKnowledgeBaseById.pending, (state) => {
state.current.status = 'loading';
})
.addCase(getKnowledgeBaseById.fulfilled, (state, action) => {
state.current.status = 'succeeded';
state.current.data = action.payload;
state.current.error = null;
})
.addCase(getKnowledgeBaseById.rejected, (state, action) => {
state.current.status = 'failed';
state.current.error = action.payload;
})
// Create knowledge base
// 创建知识库
.addCase(createKnowledgeBase.pending, (state) => {
state.operations.status = 'loading';
state.operations.operationType = 'create';
state.loading = true;
state.error = null;
})
.addCase(createKnowledgeBase.fulfilled, (state, action) => {
state.operations.status = 'succeeded';
// Don't add to list here - better to refetch the list to ensure consistency
state.operations.error = null;
state.loading = false;
state.editStatus = 'successful';
// 不需要更新 knowledgeBases因为创建后会跳转到详情页
})
.addCase(createKnowledgeBase.rejected, (state, action) => {
state.operations.status = 'failed';
state.operations.error = action.payload;
state.loading = false;
state.error = action.payload || 'Failed to create knowledge base';
state.editStatus = 'failed';
})
// Update knowledge base
// 更新知识库
.addCase(updateKnowledgeBase.pending, (state) => {
state.operations.status = 'loading';
state.operations.operationType = 'update';
state.loading = true;
state.error = null;
})
.addCase(updateKnowledgeBase.fulfilled, (state, action) => {
state.operations.status = 'succeeded';
// Update in list if present
const index = state.list.data.items.findIndex((item) => item.id === action.payload.id);
if (index !== -1) {
state.list.data.items[index] = action.payload;
}
// Update in search results if present
const searchIndex = state.search.data.items.findIndex((item) => item.id === action.payload.id);
if (searchIndex !== -1) {
state.search.data.items[searchIndex] = action.payload;
}
// Update current if it's the same knowledge base
if (state.current.data && state.current.data.id === action.payload.id) {
state.current.data = action.payload;
}
state.operations.error = null;
state.loading = false;
state.currentKnowledgeBase = action.payload;
state.editStatus = 'successful';
})
.addCase(updateKnowledgeBase.rejected, (state, action) => {
state.operations.status = 'failed';
state.operations.error = action.payload;
state.loading = false;
state.error = action.payload || 'Failed to update knowledge base';
state.editStatus = 'failed';
})
// Delete knowledge base
// 删除知识库
.addCase(deleteKnowledgeBase.pending, (state) => {
state.operations.status = 'loading';
state.operations.operationType = 'delete';
state.loading = true;
state.error = null;
})
.addCase(deleteKnowledgeBase.fulfilled, (state, action) => {
state.operations.status = 'succeeded';
// Remove from list if present
state.list.data.items = state.list.data.items.filter((item) => item.id !== action.payload);
// Remove from search results if present
state.search.data.items = state.search.data.items.filter((item) => item.id !== action.payload);
// Reset current if it's the same knowledge base
if (state.current.data && state.current.data.id === action.payload) {
state.current.data = null;
}
state.operations.error = null;
state.loading = false;
state.knowledgeBases = state.knowledgeBases.filter((kb) => kb.id !== action.meta.arg.knowledgeBaseId);
})
.addCase(deleteKnowledgeBase.rejected, (state, action) => {
state.operations.status = 'failed';
state.operations.error = action.payload;
state.loading = false;
state.error = action.payload || 'Failed to delete knowledge base';
})
// 修改知识库类型
.addCase(changeKnowledgeBaseType.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(changeKnowledgeBaseType.fulfilled, (state, action) => {
state.loading = false;
if (state.currentKnowledgeBase) {
state.currentKnowledgeBase = {
...state.currentKnowledgeBase,
type: action.payload.type,
department: action.payload.department,
group: action.payload.group,
};
}
state.editStatus = 'successful';
})
.addCase(changeKnowledgeBaseType.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || 'Failed to change knowledge base type';
state.editStatus = 'failed';
})
// 搜索知识库
.addCase(searchKnowledgeBases.pending, (state) => {
state.searchLoading = true;
state.error = null;
})
.addCase(searchKnowledgeBases.fulfilled, (state, action) => {
state.searchLoading = false;
if (action.payload && action.payload.code === 200) {
state.searchResults = action.payload.data.items || [];
} else {
state.searchResults = action.payload.items || [];
}
})
.addCase(searchKnowledgeBases.rejected, (state, action) => {
state.searchLoading = false;
state.error = action.payload || 'Failed to search knowledge bases';
})
// 申请知识库访问权限
.addCase(requestKnowledgeBaseAccess.pending, (state) => {
state.requestAccessStatus = 'loading';
})
.addCase(requestKnowledgeBaseAccess.fulfilled, (state) => {
state.requestAccessStatus = 'successful';
})
.addCase(requestKnowledgeBaseAccess.rejected, (state) => {
state.requestAccessStatus = 'failed';
});
},
});
export const { resetOperationStatus, resetCurrentKnowledgeBase, resetSearchState } = knowledgeBaseSlice.actions;
const knowledgeBaseReducer = knowledgeBaseSlice.reducer;
export default knowledgeBaseReducer;
export const { clearCurrentKnowledgeBase, clearSearchResults, clearEditStatus } = knowledgeBaseSlice.actions;
export default knowledgeBaseSlice.reducer;

View File

@ -1,5 +1,6 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { get, post, put, del } from '../../services/api';
import { showNotification } from '../notification.slice';
/**
* Fetch knowledge bases with pagination
@ -30,24 +31,26 @@ export const fetchKnowledgeBases = createAsyncThunk(
* @param {number} params.page - Page number (default: 1)
* @param {number} params.page_size - Page size (default: 10)
*/
export const searchKnowledgeBases = createAsyncThunk(
'knowledgeBase/searchKnowledgeBases',
async ({ keyword, page = 1, page_size = 10 }, { rejectWithValue }) => {
try {
const response = await get('/knowledge-bases/search/', {
params: { keyword, page, page_size },
});
export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => {
try {
const { keyword, page = 1, page_size = 10 } = params;
const response = await get('/knowledge-bases/search/', {
params: {
keyword,
page,
page_size,
},
});
// 处理新的返回格式
if (response.data && response.data.code === 200) {
return response.data.data;
}
// 处理新的返回格式
if (response.data && response.data.code === 200) {
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to search knowledge bases');
}
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || error.message);
}
);
});
/**
* Create a new knowledge base
@ -80,7 +83,7 @@ export const getKnowledgeBaseById = createAsyncThunk(
const response = await get(`/knowledge-bases/${id}/`);
// 处理新的返回格式
if (response.data && response.data.code === 200) {
return response.data.data.knowledge_base;
return response.data.data;
}
return response.data;
} catch (error) {
@ -128,3 +131,64 @@ export const deleteKnowledgeBase = createAsyncThunk(
}
}
);
/**
* Change knowledge base type
* @param {Object} params - Parameters
* @param {string} params.id - Knowledge base ID
* @param {string} params.type - New knowledge base type
* @param {string} params.department - User department
* @param {string} params.group - User group
*/
export const changeKnowledgeBaseType = createAsyncThunk(
'knowledgeBase/changeType',
async ({ id, type, department, group }, { rejectWithValue }) => {
try {
const response = await post(`/knowledge-bases/${id}/change_type/`, {
type,
department,
group,
});
// 处理新的返回格式
if (response.data && response.data.code === 200) {
return response.data.data;
}
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || '修改知识库类型失败');
}
}
);
/**
* 申请知识库访问权限
* @param {Object} params - 参数
* @param {string} params.knowledgeBaseId - 知识库ID
* @returns {Promise} - Promise对象
*/
export const requestKnowledgeBaseAccess = createAsyncThunk(
'knowledgeBase/requestAccess',
async (params, { rejectWithValue, dispatch }) => {
try {
const { knowledgeBaseId } = params;
const response = await post(`/knowledge-bases/${knowledgeBaseId}/request_access/`);
dispatch(
showNotification({
type: 'success',
message: '权限申请已发送,请等待管理员审核',
})
);
return response.data;
} catch (error) {
dispatch(
showNotification({
type: 'danger',
message: error.response?.data?.detail || '权限申请失败,请稍后重试',
})
);
return rejectWithValue(error.response?.data || error.message);
}
}
);

View File

@ -14,7 +14,7 @@ export default defineConfig(({ mode }) => {
port: env.VITE_PORT,
proxy: {
'/api': {
target: env.VITE_API_URL || 'http://124.222.236.141:58000',
target: env.VITE_API_URL || 'http://81.69.223.133:3000',
changeOrigin: true,
},
},