mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 05:26:07 +08:00
[dev]register && create knowledgebase api test
This commit is contained in:
parent
6f48ff656b
commit
167b06315d
6
package-lock.json
generated
6
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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'>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>`,
|
||||
};
|
||||
|
@ -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 />
|
||||
|
||||
|
@ -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 */}
|
||||
|
@ -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>
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
|
53
src/pages/KnowledgeBase/KnowledgeBase.css
Normal file
53
src/pages/KnowledgeBase/KnowledgeBase.css
Normal 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%;
|
||||
}
|
||||
}
|
@ -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}
|
||||
/>
|
||||
|
||||
{/* 申请权限弹窗 */}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user