mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 09:48:14 +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",
|
"axios": "^1.8.1",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
@ -3370,6 +3371,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
|
@ -11,10 +11,48 @@ import SvgIcon from './SvgIcon';
|
|||||||
* @param {Function} props.onClose - 关闭弹窗的回调函数
|
* @param {Function} props.onClose - 关闭弹窗的回调函数
|
||||||
* @param {Function} props.onChange - 表单输入变化的回调函数
|
* @param {Function} props.onChange - 表单输入变化的回调函数
|
||||||
* @param {Function} props.onSubmit - 提交表单的回调函数
|
* @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;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className='modal-backdrop'
|
className='modal-backdrop'
|
||||||
@ -82,36 +120,29 @@ const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, on
|
|||||||
<label className='form-label'>
|
<label className='form-label'>
|
||||||
知识库类型 <span className='text-danger'>*</span>
|
知识库类型 <span className='text-danger'>*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className='d-flex gap-3'>
|
<div className='d-flex flex-wrap gap-3'>
|
||||||
<div className='form-check'>
|
{availableTypes.map((type, index) => (
|
||||||
<input
|
<div className='form-check' key={index}>
|
||||||
className='form-check-input'
|
<input
|
||||||
type='radio'
|
className='form-check-input'
|
||||||
name='type'
|
type='radio'
|
||||||
id='typePrivate'
|
name='type'
|
||||||
value='private'
|
id={`type${type.value}`}
|
||||||
checked={formData.type === 'private'}
|
value={type.value}
|
||||||
onChange={onChange}
|
checked={formData.type === type.value}
|
||||||
/>
|
onChange={onChange}
|
||||||
<label className='form-check-label' htmlFor='typePrivate'>
|
/>
|
||||||
私有知识库
|
<label className='form-check-label' htmlFor={`type${type.value}`}>
|
||||||
</label>
|
{type.label}
|
||||||
</div>
|
</label>
|
||||||
<div className='form-check'>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
|
{!isAdmin && !isLeader && (
|
||||||
|
<small className='text-muted d-block mt-1'>
|
||||||
|
注意:您当前只能创建私有知识库。其他类型需要更高权限。
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
{formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>}
|
{formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import SvgIcon from './SvgIcon';
|
import SvgIcon from './SvgIcon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,6 +11,10 @@ import SvgIcon from './SvgIcon';
|
|||||||
* @param {Function} props.onClearSearch - 清除搜索的回调函数
|
* @param {Function} props.onClearSearch - 清除搜索的回调函数
|
||||||
* @param {string} props.placeholder - 搜索框占位文本
|
* @param {string} props.placeholder - 搜索框占位文本
|
||||||
* @param {string} props.className - 额外的 CSS 类名
|
* @param {string} props.className - 额外的 CSS 类名
|
||||||
|
* @param {Array} props.searchResults - 搜索结果
|
||||||
|
* @param {boolean} props.isSearchLoading - 搜索是否正在加载
|
||||||
|
* @param {Function} props.onResultClick - 点击搜索结果的回调
|
||||||
|
* @param {Function} props.onRequestAccess - 申请权限的回调
|
||||||
*/
|
*/
|
||||||
const SearchBar = ({
|
const SearchBar = ({
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
@ -20,22 +24,179 @@ const SearchBar = ({
|
|||||||
onClearSearch,
|
onClearSearch,
|
||||||
placeholder = '搜索...',
|
placeholder = '搜索...',
|
||||||
className = 'w-50',
|
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 (
|
return (
|
||||||
<form className={`d-flex ${className}`} onSubmit={onSearch}>
|
<div className={`position-relative ${className}`} ref={searchRef}>
|
||||||
<input
|
<form className='d-flex' onSubmit={handleSubmit}>
|
||||||
type='text'
|
<div className='input-group'>
|
||||||
className='form-control'
|
<input
|
||||||
placeholder={placeholder}
|
ref={inputRef}
|
||||||
value={searchKeyword}
|
type='text'
|
||||||
onChange={onSearchChange}
|
className='form-control'
|
||||||
/>
|
placeholder={placeholder}
|
||||||
{isSearching && (
|
value={searchKeyword}
|
||||||
<button type='button' className='btn btn-outline-dark ms-2' onClick={onClearSearch}>
|
onChange={handleInputChange}
|
||||||
清除
|
onFocus={handleFocus}
|
||||||
</button>
|
/>
|
||||||
|
{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'>
|
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' />
|
<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>`,
|
</svg>`,
|
||||||
lock: `<svg xmlns='http://www.w3.org/2000/svg' height='14' width='12.25' viewBox='0 0 448 512' fill='currentColor'>
|
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<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' />
|
<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>`,
|
</svg>`,
|
||||||
'stack-fill': `<svg
|
'stack-fill': `<svg
|
||||||
t='1741043402869'
|
t='1741043402869'
|
||||||
class='icon'
|
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>`,
|
send: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z"/></svg>`,
|
||||||
search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>`,
|
search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>`,
|
||||||
bell: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill='currentColor'><path d="M224 0c-17.7 0-32 14.3-32 32l0 19.2C119 66 64 130.6 64 208l0 18.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S19.4 416 32 416l384 0c12.6 0 24-7.4 29.2-18.9s3.1-25-5.3-34.4l-7.4-8.3C401.3 319.2 384 273.9 384 226.8l0-18.8c0-77.4-55-142-128-156.8L256 32c0-17.7-14.3-32-32-32zm45.3 493.3c12-12 18.7-28.3 18.7-45.3l-64 0-64 0c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z"/></svg>`,
|
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 { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { showNotification } from '../../../store/notification.slice';
|
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 SvgIcon from '../../../components/SvgIcon';
|
||||||
import DatasetTab from './DatasetTab';
|
import DatasetTab from './DatasetTab';
|
||||||
import SettingsTab from './SettingsTab';
|
import SettingsTab from './SettingsTab';
|
||||||
@ -14,16 +14,15 @@ export default function KnowledgeBaseDetail() {
|
|||||||
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
|
const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
|
||||||
|
|
||||||
// Get knowledge base details from Redux store
|
// Get knowledge base details from Redux store
|
||||||
const { data, status } = useSelector((state) => state.knowledgeBase.list);
|
const { data: knowledgeBase, status, error } = useSelector((state) => state.knowledgeBase.detail);
|
||||||
const knowledgeBase = data?.items?.find((kb) => kb.id === id);
|
|
||||||
const isLoading = status === 'loading';
|
const isLoading = status === 'loading';
|
||||||
|
|
||||||
// Fetch knowledge bases if not available
|
// Fetch knowledge base details when component mounts or ID changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data?.items?.length && status !== 'loading') {
|
if (id) {
|
||||||
dispatch(fetchKnowledgeBases());
|
dispatch(getKnowledgeBaseById(id));
|
||||||
}
|
}
|
||||||
}, [dispatch, data, status]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
// Update active tab when URL changes
|
// Update active tab when URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -32,18 +31,18 @@ export default function KnowledgeBaseDetail() {
|
|||||||
}
|
}
|
||||||
}, [tab]);
|
}, [tab]);
|
||||||
|
|
||||||
// If knowledge base not found in Redux store, show notification and redirect
|
// If knowledge base not found, show notification and redirect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!knowledgeBase && data?.items?.length > 0 && !isLoading) {
|
if (status === 'failed' && error) {
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: '未找到知识库,请返回知识库列表',
|
message: `获取知识库失败: ${error.message || '未找到知识库'}`,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
navigate('/knowledge-base');
|
navigate('/knowledge-base');
|
||||||
}
|
}
|
||||||
}, [knowledgeBase, data, isLoading, dispatch, navigate]);
|
}, [status, error, dispatch, navigate]);
|
||||||
|
|
||||||
// Handle tab change
|
// Handle tab change
|
||||||
const handleTabChange = (tab) => {
|
const handleTabChange = (tab) => {
|
||||||
@ -69,9 +68,7 @@ export default function KnowledgeBaseDetail() {
|
|||||||
<div className='col-md-3 col-lg-2 border-end'>
|
<div className='col-md-3 col-lg-2 border-end'>
|
||||||
<div className='py-4'>
|
<div className='py-4'>
|
||||||
<div className='h4 mb-3 text-center'>{knowledgeBase.name}</div>
|
<div className='h4 mb-3 text-center'>{knowledgeBase.name}</div>
|
||||||
<p className='text-center text-muted small mb-4'>
|
<p className='text-center text-muted small mb-4'>{knowledgeBase.desc || ''}</p>
|
||||||
{knowledgeBase.desc || ''}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { showNotification } from '../../../store/notification.slice';
|
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';
|
import Breadcrumb from './components/Breadcrumb';
|
||||||
@ -10,25 +14,98 @@ import KnowledgeBaseForm from './components/KnowledgeBaseForm';
|
|||||||
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
import DeleteConfirmModal from './components/DeleteConfirmModal';
|
||||||
import UserPermissionsManager from './components/UserPermissionsManager';
|
import UserPermissionsManager from './components/UserPermissionsManager';
|
||||||
|
|
||||||
|
// 部门和组别的映射关系
|
||||||
|
const departmentGroups = {
|
||||||
|
技术部: ['开发组', '测试组', '运维组', '架构组', '安全组'],
|
||||||
|
产品部: ['产品规划组', '用户研究组', '交互设计组', '项目管理组'],
|
||||||
|
市场部: ['品牌推广组', '市场调研组', '客户关系组', '社交媒体组'],
|
||||||
|
行政部: ['人事组', '财务组', '行政管理组', '后勤组'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 部门列表
|
||||||
|
const departments = Object.keys(departmentGroups);
|
||||||
|
|
||||||
export default function SettingsTab({ knowledgeBase }) {
|
export default function SettingsTab({ knowledgeBase }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const currentUser = useSelector((state) => state.auth.user);
|
||||||
|
const isAdmin = currentUser?.role === 'admin';
|
||||||
|
|
||||||
// State for knowledge base form
|
// State for knowledge base form
|
||||||
const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({
|
const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({
|
||||||
name: knowledgeBase.name,
|
name: knowledgeBase.name,
|
||||||
desc: knowledgeBase.desc || knowledgeBase.description || '',
|
desc: knowledgeBase.desc || knowledgeBase.description || '',
|
||||||
type: knowledgeBase.type || 'private', // 默认为私有知识库
|
type: knowledgeBase.type || 'private', // 默认为私有知识库
|
||||||
|
original_type: knowledgeBase.type || 'private', // 存储原始类型用于比较
|
||||||
department: knowledgeBase.department || '',
|
department: knowledgeBase.department || '',
|
||||||
group: knowledgeBase.group || '',
|
group: knowledgeBase.group || '',
|
||||||
|
original_department: knowledgeBase.department || '',
|
||||||
|
original_group: knowledgeBase.group || '',
|
||||||
});
|
});
|
||||||
const [formErrors, setFormErrors] = useState({});
|
const [formErrors, setFormErrors] = useState({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = 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
|
// Handle knowledge base form input change
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
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) => ({
|
setKnowledgeBaseForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
@ -59,10 +136,87 @@ export default function SettingsTab({ knowledgeBase }) {
|
|||||||
errors.type = '请选择知识库类型';
|
errors.type = '请选择知识库类型';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAdmin && !knowledgeBaseForm.department) {
|
||||||
|
errors.department = '请选择部门';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAdmin && !knowledgeBaseForm.group) {
|
||||||
|
errors.group = '请选择组别';
|
||||||
|
}
|
||||||
|
|
||||||
setFormErrors(errors);
|
setFormErrors(errors);
|
||||||
return Object.keys(errors).length === 0;
|
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
|
// Handle form submission
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -74,7 +228,13 @@ export default function SettingsTab({ knowledgeBase }) {
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
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(
|
dispatch(
|
||||||
updateKnowledgeBase({
|
updateKnowledgeBase({
|
||||||
id: knowledgeBase.id,
|
id: knowledgeBase.id,
|
||||||
@ -82,9 +242,6 @@ export default function SettingsTab({ knowledgeBase }) {
|
|||||||
name: knowledgeBaseForm.name,
|
name: knowledgeBaseForm.name,
|
||||||
desc: knowledgeBaseForm.desc,
|
desc: knowledgeBaseForm.desc,
|
||||||
description: knowledgeBaseForm.desc, // Add description field for compatibility
|
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}
|
onInputChange={handleInputChange}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onDelete={() => setShowDeleteConfirm(true)}
|
onDelete={() => setShowDeleteConfirm(true)}
|
||||||
|
onTypeChange={handleTypeChange}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
departments={departments}
|
||||||
|
availableGroups={availableGroups}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* User Permissions Manager */}
|
{/* 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,
|
onUpload,
|
||||||
}) => {
|
}) => {
|
||||||
const fileInputRef = useRef(null);
|
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;
|
if (!show) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={modalRef}
|
||||||
className='modal-backdrop'
|
className='modal-backdrop'
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@ -54,11 +87,17 @@ const FileUploadModal = ({
|
|||||||
fileErrors.file ? 'border-danger' : 'border-dashed'
|
fileErrors.file ? 'border-danger' : 'border-dashed'
|
||||||
}`}
|
}`}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={onUploadAreaClick}
|
onClick={handleUploadAreaClick}
|
||||||
onDrop={onFileDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={onDragOver}
|
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 ? (
|
{newFile.file ? (
|
||||||
<div>
|
<div>
|
||||||
<p className='mb-1'>已选择文件:</p>
|
<p className='mb-1'>已选择文件:</p>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库表单组件
|
* 知识库表单组件
|
||||||
@ -10,7 +11,50 @@ const KnowledgeBaseForm = ({
|
|||||||
onInputChange,
|
onInputChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onDelete,
|
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 (
|
return (
|
||||||
<div className='card border-0 shadow-sm'>
|
<div className='card border-0 shadow-sm'>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
@ -47,69 +91,151 @@ const KnowledgeBaseForm = ({
|
|||||||
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
|
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mb-3'>
|
<div className='mb-4'>
|
||||||
<label className='form-label'>知识库类型</label>
|
<label className='form-label'>
|
||||||
<div className='form-check'>
|
知识库类型 <span className='text-danger'>*</span>
|
||||||
<input
|
</label>
|
||||||
className='form-check-input'
|
<div className='d-flex flex-column gap-2'>
|
||||||
type='radio'
|
{availableTypes.map((type) => (
|
||||||
name='type'
|
<div className='form-check' key={type.value}>
|
||||||
id='typePrivate'
|
<input
|
||||||
value='private'
|
className='form-check-input'
|
||||||
checked={formData.type === 'private'}
|
type='radio'
|
||||||
onChange={onInputChange}
|
name='type'
|
||||||
/>
|
id={`type${type.value}`}
|
||||||
<label className='form-check-label' htmlFor='typePrivate'>
|
value={type.value}
|
||||||
私有知识库
|
checked={formData.type === type.value}
|
||||||
</label>
|
onChange={onInputChange}
|
||||||
</div>
|
disabled={currentUser?.role === 'member'} // 普通成员无法修改类型
|
||||||
<div className='form-check'>
|
/>
|
||||||
<input
|
<label className='form-check-label' htmlFor={`type${type.value}`}>
|
||||||
className='form-check-input'
|
{type.label}
|
||||||
type='radio'
|
</label>
|
||||||
name='type'
|
</div>
|
||||||
id='typePublic'
|
))}
|
||||||
value='public'
|
|
||||||
checked={formData.type === 'public'}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
<label className='form-check-label' htmlFor='typePublic'>
|
|
||||||
公共知识库
|
|
||||||
</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>
|
||||||
|
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<label htmlFor='department' className='form-label'>
|
<label htmlFor='department' className='form-label'>
|
||||||
部门
|
部门 {isAdmin && <span className='text-danger'>*</span>}
|
||||||
</label>
|
</label>
|
||||||
<input
|
{isAdmin ? (
|
||||||
type='text'
|
<>
|
||||||
className='form-control bg-light'
|
<select
|
||||||
id='department'
|
className={`form-select ${formErrors.department ? 'is-invalid' : ''}`}
|
||||||
name='department'
|
id='department'
|
||||||
value={formData.department || ''}
|
name='department'
|
||||||
readOnly
|
value={formData.department || ''}
|
||||||
/>
|
onChange={onInputChange}
|
||||||
<small className='text-muted'>部门信息根据知识库创建者自动填写</small>
|
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>
|
||||||
|
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<label htmlFor='group' className='form-label'>
|
<label htmlFor='group' className='form-label'>
|
||||||
组别
|
组别 {isAdmin && <span className='text-danger'>*</span>}
|
||||||
</label>
|
</label>
|
||||||
<input
|
{isAdmin ? (
|
||||||
type='text'
|
<>
|
||||||
className='form-control bg-light'
|
<select
|
||||||
id='group'
|
className={`form-select ${formErrors.group ? 'is-invalid' : ''}`}
|
||||||
name='group'
|
id='group'
|
||||||
value={formData.group || ''}
|
name='group'
|
||||||
readOnly
|
value={formData.group || ''}
|
||||||
/>
|
onChange={onInputChange}
|
||||||
<small className='text-muted'>组别信息根据知识库创建者自动填写</small>
|
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>
|
||||||
|
|
||||||
<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}>
|
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
|
||||||
{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 { useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { showNotification } from '../../store/notification.slice';
|
import { showNotification } from '../../store/notification.slice';
|
||||||
@ -6,14 +6,17 @@ import {
|
|||||||
fetchKnowledgeBases,
|
fetchKnowledgeBases,
|
||||||
searchKnowledgeBases,
|
searchKnowledgeBases,
|
||||||
createKnowledgeBase,
|
createKnowledgeBase,
|
||||||
|
deleteKnowledgeBase,
|
||||||
|
requestKnowledgeBaseAccess,
|
||||||
} from '../../store/knowledgeBase/knowledgeBase.thunks';
|
} 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 SvgIcon from '../../components/SvgIcon';
|
||||||
import { requestKnowledgeBaseAccess } from '../../services/permissionService';
|
|
||||||
import AccessRequestModal from '../../components/AccessRequestModal';
|
import AccessRequestModal from '../../components/AccessRequestModal';
|
||||||
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
|
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
|
||||||
import Pagination from '../../components/Pagination';
|
import Pagination from '../../components/Pagination';
|
||||||
import SearchBar from '../../components/SearchBar';
|
import SearchBar from '../../components/SearchBar';
|
||||||
|
import ApiModeSwitch from '../../components/ApiModeSwitch';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
// 导入拆分的组件
|
// 导入拆分的组件
|
||||||
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
||||||
@ -29,6 +32,7 @@ export default function KnowledgeBase() {
|
|||||||
title: '',
|
title: '',
|
||||||
});
|
});
|
||||||
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
|
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
|
||||||
|
const [createdKnowledgeBaseId, setCreatedKnowledgeBaseId] = useState(null);
|
||||||
|
|
||||||
// 获取当前用户信息
|
// 获取当前用户信息
|
||||||
const currentUser = useSelector((state) => state.auth.user);
|
const currentUser = useSelector((state) => state.auth.user);
|
||||||
@ -52,20 +56,23 @@ export default function KnowledgeBase() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get knowledge bases from Redux store
|
// Get knowledge bases from Redux store
|
||||||
const { data, status, error } = useSelector((state) => state.knowledgeBase.list);
|
// 更新为新的Redux状态结构
|
||||||
const {
|
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases);
|
||||||
data: searchData,
|
const loading = useSelector((state) => state.knowledgeBase.loading);
|
||||||
status: searchStatus,
|
const paginationData = useSelector((state) => state.knowledgeBase.pagination);
|
||||||
error: searchError,
|
const error = useSelector((state) => state.knowledgeBase.error);
|
||||||
keyword: storeKeyword,
|
const operationStatus = useSelector((state) => state.knowledgeBase.editStatus);
|
||||||
} = useSelector((state) => state.knowledgeBase.search);
|
const operationError = useSelector((state) => state.knowledgeBase.error);
|
||||||
const { status: operationStatus, error: operationError } = useSelector((state) => state.knowledgeBase.operations);
|
|
||||||
|
// 从Redux获取搜索结果和加载状态
|
||||||
|
const searchResults = useSelector((state) => state.knowledgeBase.searchResults);
|
||||||
|
const searchLoading = useSelector((state) => state.knowledgeBase.searchLoading);
|
||||||
|
|
||||||
// Determine which data to display based on search state
|
// Determine which data to display based on search state
|
||||||
const displayData = isSearching ? searchData?.items : data?.items;
|
const displayData = isSearching ? searchResults : knowledgeBases;
|
||||||
const displayTotal = isSearching ? searchData?.total : data?.total;
|
const displayTotal = paginationData.total;
|
||||||
const displayStatus = isSearching ? searchStatus : status;
|
const displayStatus = loading ? 'loading' : 'succeeded';
|
||||||
const displayError = isSearching ? searchError : error;
|
const displayError = error;
|
||||||
|
|
||||||
// Fetch knowledge bases when component mounts or pagination changes
|
// Fetch knowledge bases when component mounts or pagination changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -82,9 +89,35 @@ export default function KnowledgeBase() {
|
|||||||
}
|
}
|
||||||
}, [dispatch, pagination.page, pagination.page_size, isSearching, searchKeyword]);
|
}, [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
|
// Handle search input change
|
||||||
const handleSearchInputChange = (e) => {
|
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
|
// Handle search submit
|
||||||
@ -112,7 +145,7 @@ export default function KnowledgeBase() {
|
|||||||
setSearchKeyword('');
|
setSearchKeyword('');
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
|
setPagination((prev) => ({ ...prev, page: 1 })); // Reset to first page
|
||||||
dispatch(resetSearchState());
|
dispatch(clearSearchResults());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show loading state while fetching data
|
// Show loading state while fetching data
|
||||||
@ -132,13 +165,9 @@ export default function KnowledgeBase() {
|
|||||||
|
|
||||||
// Show notification for operation status
|
// Show notification for operation status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (operationStatus === 'succeeded') {
|
if (operationStatus === 'successful') {
|
||||||
dispatch(
|
// 操作成功通知由具体函数处理,这里只刷新列表
|
||||||
showNotification({
|
|
||||||
message: '操作成功',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// Refresh the list after successful operation
|
// Refresh the list after successful operation
|
||||||
if (isSearching && searchKeyword.trim()) {
|
if (isSearching && searchKeyword.trim()) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -184,6 +213,34 @@ export default function KnowledgeBase() {
|
|||||||
return;
|
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) => ({
|
setNewKnowledgeBase((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
@ -217,23 +274,73 @@ export default function KnowledgeBase() {
|
|||||||
return Object.keys(errors).length === 0;
|
return Object.keys(errors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateKnowledgeBase = () => {
|
const handleCreateKnowledgeBase = async () => {
|
||||||
// Validate form
|
// Validate form
|
||||||
if (!validateCreateForm()) {
|
if (!validateCreateForm()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch create knowledge base action
|
try {
|
||||||
dispatch(
|
// Dispatch create knowledge base action
|
||||||
createKnowledgeBase({
|
const resultAction = await dispatch(
|
||||||
name: newKnowledgeBase.name,
|
createKnowledgeBase({
|
||||||
desc: newKnowledgeBase.desc,
|
name: newKnowledgeBase.name,
|
||||||
description: newKnowledgeBase.desc,
|
desc: newKnowledgeBase.desc,
|
||||||
type: newKnowledgeBase.type,
|
description: newKnowledgeBase.desc,
|
||||||
department: newKnowledgeBase.department,
|
type: newKnowledgeBase.type,
|
||||||
group: newKnowledgeBase.group,
|
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
|
// Reset form and close modal
|
||||||
setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
|
setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
|
||||||
@ -297,7 +404,25 @@ export default function KnowledgeBase() {
|
|||||||
const handleDelete = (e, id) => {
|
const handleDelete = (e, id) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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
|
// Calculate total pages
|
||||||
@ -305,17 +430,31 @@ export default function KnowledgeBase() {
|
|||||||
|
|
||||||
// 打开创建知识库弹窗
|
// 打开创建知识库弹窗
|
||||||
const handleOpenCreateModal = () => {
|
const handleOpenCreateModal = () => {
|
||||||
|
// 默认知识库类型基于用户角色
|
||||||
|
let defaultType = 'private';
|
||||||
|
|
||||||
// 确保部门和组别字段使用当前用户的信息
|
// 确保部门和组别字段使用当前用户的信息
|
||||||
setNewKnowledgeBase((prev) => ({
|
setNewKnowledgeBase((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
department: currentUser?.department || '',
|
department: currentUser?.department || '',
|
||||||
group: currentUser?.group || '',
|
group: currentUser?.group || '',
|
||||||
|
type: defaultType,
|
||||||
}));
|
}));
|
||||||
setShowCreateModal(true);
|
setShowCreateModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理点击搜索结果
|
||||||
|
const handleSearchResultClick = (id, permissions) => {
|
||||||
|
if (permissions?.can_read) {
|
||||||
|
navigate(`/knowledge-base/${id}/datasets`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='knowledge-base container my-4'>
|
<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'>
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
@ -324,6 +463,10 @@ export default function KnowledgeBase() {
|
|||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onClearSearch={handleClearSearch}
|
onClearSearch={handleClearSearch}
|
||||||
placeholder='搜索知识库...'
|
placeholder='搜索知识库...'
|
||||||
|
searchResults={searchResults}
|
||||||
|
isSearchLoading={searchLoading}
|
||||||
|
onResultClick={handleSearchResultClick}
|
||||||
|
onRequestAccess={handleRequestAccess}
|
||||||
/>
|
/>
|
||||||
<button className='btn btn-dark d-flex align-items-center gap-1' onClick={handleOpenCreateModal}>
|
<button className='btn btn-dark d-flex align-items-center gap-1' onClick={handleOpenCreateModal}>
|
||||||
<SvgIcon className={'plus'} />
|
<SvgIcon className={'plus'} />
|
||||||
@ -333,7 +476,7 @@ export default function KnowledgeBase() {
|
|||||||
|
|
||||||
{isSearching && (
|
{isSearching && (
|
||||||
<div className='alert alert-info'>
|
<div className='alert alert-info'>
|
||||||
搜索结果: "{storeKeyword}" - 找到 {displayTotal} 个知识库
|
搜索结果: "{searchKeyword}" - 找到 {displayTotal} 个知识库
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -371,10 +514,11 @@ export default function KnowledgeBase() {
|
|||||||
show={showCreateModal}
|
show={showCreateModal}
|
||||||
formData={newKnowledgeBase}
|
formData={newKnowledgeBase}
|
||||||
formErrors={formErrors}
|
formErrors={formErrors}
|
||||||
isSubmitting={operationStatus === 'loading'}
|
isSubmitting={loading}
|
||||||
onClose={() => setShowCreateModal(false)}
|
onClose={() => setShowCreateModal(false)}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onSubmit={handleCreateKnowledgeBase}
|
onSubmit={handleCreateKnowledgeBase}
|
||||||
|
currentUser={currentUser}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 申请权限弹窗 */}
|
{/* 申请权限弹窗 */}
|
||||||
|
@ -3,7 +3,6 @@ import { useSelector } from 'react-redux';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import PendingRequests from './components/PendingRequests';
|
import PendingRequests from './components/PendingRequests';
|
||||||
import UserPermissions from './components/UserPermissions';
|
import UserPermissions from './components/UserPermissions';
|
||||||
import ApiModeSwitch from '../../components/ApiModeSwitch';
|
|
||||||
import './Permissions.css';
|
import './Permissions.css';
|
||||||
|
|
||||||
export default function PermissionsPage() {
|
export default function PermissionsPage() {
|
||||||
@ -19,10 +18,6 @@ export default function PermissionsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='permissions-container'>
|
<div className='permissions-container'>
|
||||||
{/* <div className='api-mode-control mb-3'>
|
|
||||||
<ApiModeSwitch />
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<div className='permissions-section mb-4'>
|
<div className='permissions-section mb-4'>
|
||||||
<PendingRequests />
|
<PendingRequests />
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,21 +3,56 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { checkAuthThunk, signupThunk } from '../../store/auth/auth.thunk';
|
import { checkAuthThunk, signupThunk } from '../../store/auth/auth.thunk';
|
||||||
|
|
||||||
|
// 部门和组别映射关系
|
||||||
|
const departmentGroups = {
|
||||||
|
'技术部': ['开发组', '测试组', '运维组'],
|
||||||
|
'产品部': ['产品规划组', '用户研究组', '交互设计组', '项目管理组'],
|
||||||
|
'市场部': ['品牌推广组', '市场调研组', '客户关系组', '社交媒体组'],
|
||||||
|
'行政部': ['人事组', '财务组', '行政管理组', '后勤组']
|
||||||
|
};
|
||||||
|
|
||||||
export default function Signup() {
|
export default function Signup() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [username, setUsername] = useState('');
|
const [formData, setFormData] = useState({
|
||||||
const [email, setEmail] = useState('');
|
username: '',
|
||||||
const [password, setPassword] = useState('');
|
email: '',
|
||||||
|
password: '',
|
||||||
|
name: '',
|
||||||
|
role: 'member',
|
||||||
|
department: '',
|
||||||
|
group: '',
|
||||||
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [availableGroups, setAvailableGroups] = useState([]);
|
||||||
|
|
||||||
const { user } = useSelector((state) => state.auth);
|
const { user, loading } = useSelector((state) => state.auth);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleCheckAuth();
|
handleCheckAuth();
|
||||||
}, [dispatch]);
|
}, [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 () => {
|
const handleCheckAuth = async () => {
|
||||||
console.log('signup page handleCheckAuth');
|
console.log('signup page handleCheckAuth');
|
||||||
try {
|
try {
|
||||||
@ -26,23 +61,52 @@ export default function Signup() {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清除对应的错误信息
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors({
|
||||||
|
...errors,
|
||||||
|
[name]: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors = {};
|
const newErrors = {};
|
||||||
if (!username) {
|
if (!formData.username) {
|
||||||
newErrors.username = 'Username is required';
|
newErrors.username = 'Username is required';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email) {
|
if (!formData.email) {
|
||||||
newErrors.email = 'Email is required';
|
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';
|
newErrors.email = 'Invalid email address';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password) {
|
if (!formData.password) {
|
||||||
newErrors.password = 'Password is required';
|
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';
|
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);
|
setErrors(newErrors);
|
||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
@ -50,16 +114,13 @@ export default function Signup() {
|
|||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
console.log(validateForm());
|
|
||||||
|
|
||||||
if (validateForm()) {
|
if (validateForm()) {
|
||||||
console.log('Form submitted successfully!');
|
console.log('Form submitted successfully!');
|
||||||
console.log('Username:', username);
|
console.log('Registration data:', formData);
|
||||||
console.log('Email:', email);
|
|
||||||
console.log('Password:', password);
|
|
||||||
try {
|
try {
|
||||||
await dispatch(signupThunk({ username, password, email })).unwrap();
|
await dispatch(signupThunk(formData)).unwrap();
|
||||||
navigate('/');
|
navigate('/login');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Signup failed:', error);
|
console.error('Signup failed:', error);
|
||||||
}
|
}
|
||||||
@ -79,9 +140,12 @@ export default function Signup() {
|
|||||||
type='text'
|
type='text'
|
||||||
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
|
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
|
||||||
id='username'
|
id='username'
|
||||||
placeholder='Username'
|
name='username'
|
||||||
|
placeholder='用户名'
|
||||||
|
value={formData.username}
|
||||||
required
|
required
|
||||||
onChange={(e) => setUsername(e.target.value.trim())}
|
onChange={handleInputChange}
|
||||||
|
disabled={loading}
|
||||||
></input>
|
></input>
|
||||||
{submitted && errors.username && <div className='invalid-feedback'>{errors.username}</div>}
|
{submitted && errors.username && <div className='invalid-feedback'>{errors.username}</div>}
|
||||||
</div>
|
</div>
|
||||||
@ -90,9 +154,12 @@ export default function Signup() {
|
|||||||
type='email'
|
type='email'
|
||||||
className={`form-control form-control-lg${submitted && errors.email ? ' is-invalid' : ''}`}
|
className={`form-control form-control-lg${submitted && errors.email ? ' is-invalid' : ''}`}
|
||||||
id='email'
|
id='email'
|
||||||
placeholder='Email'
|
name='email'
|
||||||
|
placeholder='邮箱'
|
||||||
|
value={formData.email}
|
||||||
required
|
required
|
||||||
onChange={(e) => setEmail(e.target.value.trim())}
|
onChange={handleInputChange}
|
||||||
|
disabled={loading}
|
||||||
></input>
|
></input>
|
||||||
{submitted && errors.email && <div className='invalid-feedback'>{errors.email}</div>}
|
{submitted && errors.email && <div className='invalid-feedback'>{errors.email}</div>}
|
||||||
</div>
|
</div>
|
||||||
@ -100,20 +167,103 @@ export default function Signup() {
|
|||||||
<input
|
<input
|
||||||
type='password'
|
type='password'
|
||||||
id='password'
|
id='password'
|
||||||
placeholder='Password'
|
name='password'
|
||||||
|
placeholder='密码'
|
||||||
|
value={formData.password}
|
||||||
required
|
required
|
||||||
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
|
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
|
||||||
aria-describedby='passwordHelpBlock'
|
aria-describedby='passwordHelpBlock'
|
||||||
onChange={(e) => setPassword(e.target.value.trim())}
|
onChange={handleInputChange}
|
||||||
|
disabled={loading}
|
||||||
></input>
|
></input>
|
||||||
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
|
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
|
||||||
</div>
|
</div>
|
||||||
<button type='submit' className='btn btn-dark btn-lg w-100'>
|
<div className='input-group has-validation'>
|
||||||
Sign Up
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<Link to='/login' className='go-to-signup w-100 link-underline-light h5 text-center'>
|
<Link to='/login' className='go-to-signup w-100 link-underline-light h5 text-center'>
|
||||||
Already have account?
|
已有账号?立即登录
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -77,7 +77,7 @@ api.interceptors.response.use(
|
|||||||
// 检查服务器状态
|
// 检查服务器状态
|
||||||
export const checkServerStatus = async () => {
|
export const checkServerStatus = async () => {
|
||||||
try {
|
try {
|
||||||
await api.get('/health-check', { timeout: 3000 });
|
// await api.get('/health-check', { timeout: 3000 });
|
||||||
isServerDown = false;
|
isServerDown = false;
|
||||||
hasCheckedServer = true;
|
hasCheckedServer = true;
|
||||||
console.log('Server connection established');
|
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 {
|
try {
|
||||||
const { message, user } = await post('/signup', config);
|
// 使用新的注册 API
|
||||||
if (!user) {
|
const { data, code } = await post('/auth/register/', userData);
|
||||||
throw new Error(message || 'Something went wrong');
|
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) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.message || 'Signup failed. Please try again.';
|
const errorMessage = error.response?.data?.message || '注册失败,请稍后重试';
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
|
@ -1,198 +1,169 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import {
|
import {
|
||||||
fetchKnowledgeBases,
|
fetchKnowledgeBases,
|
||||||
searchKnowledgeBases,
|
|
||||||
createKnowledgeBase,
|
createKnowledgeBase,
|
||||||
getKnowledgeBaseById,
|
|
||||||
updateKnowledgeBase,
|
updateKnowledgeBase,
|
||||||
deleteKnowledgeBase,
|
deleteKnowledgeBase,
|
||||||
|
changeKnowledgeBaseType,
|
||||||
|
searchKnowledgeBases,
|
||||||
|
requestKnowledgeBaseAccess,
|
||||||
} from './knowledgeBase.thunks';
|
} from './knowledgeBase.thunks';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
// List state
|
knowledgeBases: [],
|
||||||
list: {
|
currentKnowledgeBase: null,
|
||||||
data: {
|
searchResults: [],
|
||||||
items: [],
|
searchLoading: false,
|
||||||
total: 0,
|
loading: false,
|
||||||
page: 1,
|
error: null,
|
||||||
page_size: 10,
|
pagination: {
|
||||||
},
|
total: 0,
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
page: 1,
|
||||||
error: null,
|
page_size: 10,
|
||||||
},
|
total_pages: 1,
|
||||||
// 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'
|
|
||||||
},
|
},
|
||||||
|
batchPermissions: {},
|
||||||
|
batchLoading: false,
|
||||||
|
editStatus: 'idle',
|
||||||
|
requestAccessStatus: 'idle',
|
||||||
};
|
};
|
||||||
|
|
||||||
const knowledgeBaseSlice = createSlice({
|
const knowledgeBaseSlice = createSlice({
|
||||||
name: 'knowledgeBase',
|
name: 'knowledgeBase',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
resetOperationStatus: (state) => {
|
clearCurrentKnowledgeBase: (state) => {
|
||||||
state.operations = {
|
state.currentKnowledgeBase = null;
|
||||||
status: 'idle',
|
|
||||||
error: null,
|
|
||||||
operationType: null,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
resetCurrentKnowledgeBase: (state) => {
|
clearSearchResults: (state) => {
|
||||||
state.current = {
|
state.searchResults = [];
|
||||||
data: null,
|
|
||||||
status: 'idle',
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
resetSearchState: (state) => {
|
clearEditStatus: (state) => {
|
||||||
state.search = {
|
state.editStatus = 'idle';
|
||||||
data: {
|
|
||||||
items: [],
|
|
||||||
total: 0,
|
|
||||||
page: 1,
|
|
||||||
page_size: 10,
|
|
||||||
keyword: '',
|
|
||||||
},
|
|
||||||
status: 'idle',
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
// Fetch knowledge bases
|
// 获取知识库列表
|
||||||
.addCase(fetchKnowledgeBases.pending, (state) => {
|
.addCase(fetchKnowledgeBases.pending, (state) => {
|
||||||
state.list.status = 'loading';
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(fetchKnowledgeBases.fulfilled, (state, action) => {
|
.addCase(fetchKnowledgeBases.fulfilled, (state, action) => {
|
||||||
state.list.status = 'succeeded';
|
state.loading = false;
|
||||||
state.list.data = action.payload;
|
state.knowledgeBases = action.payload.items || [];
|
||||||
state.list.error = null;
|
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) => {
|
.addCase(fetchKnowledgeBases.rejected, (state, action) => {
|
||||||
state.list.status = 'failed';
|
state.loading = false;
|
||||||
state.list.error = action.payload;
|
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) => {
|
.addCase(createKnowledgeBase.pending, (state) => {
|
||||||
state.operations.status = 'loading';
|
state.loading = true;
|
||||||
state.operations.operationType = 'create';
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(createKnowledgeBase.fulfilled, (state, action) => {
|
.addCase(createKnowledgeBase.fulfilled, (state, action) => {
|
||||||
state.operations.status = 'succeeded';
|
state.loading = false;
|
||||||
// Don't add to list here - better to refetch the list to ensure consistency
|
state.editStatus = 'successful';
|
||||||
state.operations.error = null;
|
// 不需要更新 knowledgeBases,因为创建后会跳转到详情页
|
||||||
})
|
})
|
||||||
.addCase(createKnowledgeBase.rejected, (state, action) => {
|
.addCase(createKnowledgeBase.rejected, (state, action) => {
|
||||||
state.operations.status = 'failed';
|
state.loading = false;
|
||||||
state.operations.error = action.payload;
|
state.error = action.payload || 'Failed to create knowledge base';
|
||||||
|
state.editStatus = 'failed';
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update knowledge base
|
// 更新知识库
|
||||||
.addCase(updateKnowledgeBase.pending, (state) => {
|
.addCase(updateKnowledgeBase.pending, (state) => {
|
||||||
state.operations.status = 'loading';
|
state.loading = true;
|
||||||
state.operations.operationType = 'update';
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(updateKnowledgeBase.fulfilled, (state, action) => {
|
.addCase(updateKnowledgeBase.fulfilled, (state, action) => {
|
||||||
state.operations.status = 'succeeded';
|
state.loading = false;
|
||||||
// Update in list if present
|
state.currentKnowledgeBase = action.payload;
|
||||||
const index = state.list.data.items.findIndex((item) => item.id === action.payload.id);
|
state.editStatus = 'successful';
|
||||||
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;
|
|
||||||
})
|
})
|
||||||
.addCase(updateKnowledgeBase.rejected, (state, action) => {
|
.addCase(updateKnowledgeBase.rejected, (state, action) => {
|
||||||
state.operations.status = 'failed';
|
state.loading = false;
|
||||||
state.operations.error = action.payload;
|
state.error = action.payload || 'Failed to update knowledge base';
|
||||||
|
state.editStatus = 'failed';
|
||||||
})
|
})
|
||||||
|
|
||||||
// Delete knowledge base
|
// 删除知识库
|
||||||
.addCase(deleteKnowledgeBase.pending, (state) => {
|
.addCase(deleteKnowledgeBase.pending, (state) => {
|
||||||
state.operations.status = 'loading';
|
state.loading = true;
|
||||||
state.operations.operationType = 'delete';
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(deleteKnowledgeBase.fulfilled, (state, action) => {
|
.addCase(deleteKnowledgeBase.fulfilled, (state, action) => {
|
||||||
state.operations.status = 'succeeded';
|
state.loading = false;
|
||||||
// Remove from list if present
|
state.knowledgeBases = state.knowledgeBases.filter((kb) => kb.id !== action.meta.arg.knowledgeBaseId);
|
||||||
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;
|
|
||||||
})
|
})
|
||||||
.addCase(deleteKnowledgeBase.rejected, (state, action) => {
|
.addCase(deleteKnowledgeBase.rejected, (state, action) => {
|
||||||
state.operations.status = 'failed';
|
state.loading = false;
|
||||||
state.operations.error = action.payload;
|
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;
|
export const { clearCurrentKnowledgeBase, clearSearchResults, clearEditStatus } = knowledgeBaseSlice.actions;
|
||||||
const knowledgeBaseReducer = knowledgeBaseSlice.reducer;
|
|
||||||
export default knowledgeBaseReducer;
|
export default knowledgeBaseSlice.reducer;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { get, post, put, del } from '../../services/api';
|
import { get, post, put, del } from '../../services/api';
|
||||||
|
import { showNotification } from '../notification.slice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch knowledge bases with pagination
|
* 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 - Page number (default: 1)
|
||||||
* @param {number} params.page_size - Page size (default: 10)
|
* @param {number} params.page_size - Page size (default: 10)
|
||||||
*/
|
*/
|
||||||
export const searchKnowledgeBases = createAsyncThunk(
|
export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => {
|
||||||
'knowledgeBase/searchKnowledgeBases',
|
try {
|
||||||
async ({ keyword, page = 1, page_size = 10 }, { rejectWithValue }) => {
|
const { keyword, page = 1, page_size = 10 } = params;
|
||||||
try {
|
const response = await get('/knowledge-bases/search/', {
|
||||||
const response = await get('/knowledge-bases/search/', {
|
params: {
|
||||||
params: { keyword, page, page_size },
|
keyword,
|
||||||
});
|
page,
|
||||||
|
page_size,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 处理新的返回格式
|
// 处理新的返回格式
|
||||||
if (response.data && response.data.code === 200) {
|
if (response.data && response.data.code === 200) {
|
||||||
return response.data.data;
|
|
||||||
}
|
|
||||||
return response.data;
|
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
|
* Create a new knowledge base
|
||||||
@ -80,7 +83,7 @@ export const getKnowledgeBaseById = createAsyncThunk(
|
|||||||
const response = await get(`/knowledge-bases/${id}/`);
|
const response = await get(`/knowledge-bases/${id}/`);
|
||||||
// 处理新的返回格式
|
// 处理新的返回格式
|
||||||
if (response.data && response.data.code === 200) {
|
if (response.data && response.data.code === 200) {
|
||||||
return response.data.data.knowledge_base;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} 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,
|
port: env.VITE_PORT,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/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,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user