diff --git a/.gitignore b/.gitignore
index a547bf3..b260b04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+.env*
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index ac545a5..74fc24a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,12 +10,16 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.6.0",
+ "axios": "^1.8.1",
"bootstrap": "^5.3.3",
+ "crypto-js": "^4.2.0",
+ "lodash": "^4.17.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
- "redux-persist": "^6.0.0"
+ "redux-persist": "^6.0.0",
+ "uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -1535,6 +1539,11 @@
"node": ">= 0.4"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -1550,6 +1559,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axios": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
+ "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1662,7 +1681,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@@ -1786,6 +1804,17 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1820,6 +1849,11 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -1934,6 +1968,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -1950,7 +1992,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@@ -2035,7 +2076,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -2044,7 +2084,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -2080,7 +2119,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"dependencies": {
"es-errors": "^1.3.0"
},
@@ -2092,7 +2130,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@@ -2469,6 +2506,25 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -2484,6 +2540,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2502,7 +2572,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2549,7 +2618,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@@ -2573,7 +2641,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@@ -2643,7 +2710,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -2703,7 +2769,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -2715,7 +2780,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -2730,7 +2794,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -3308,6 +3371,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3339,11 +3407,29 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3676,6 +3762,11 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4456,6 +4547,18 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/vite": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
diff --git a/package.json b/package.json
index e788603..ce9ab41 100644
--- a/package.json
+++ b/package.json
@@ -12,12 +12,16 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.6.0",
+ "axios": "^1.8.1",
"bootstrap": "^5.3.3",
+ "crypto-js": "^4.2.0",
+ "lodash": "^4.17.21",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
- "redux-persist": "^6.0.0"
+ "redux-persist": "^6.0.0",
+ "uuid": "^11.1.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..9da09c4
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ OOIN 智能知识库
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/vite.svg b/public/vite.svg
index e7b8dfb..40d10df 100644
--- a/public/vite.svg
+++ b/public/vite.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index d0dde09..6263565 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,7 +1,60 @@
-import HeaderWithNav from './layouts/HeaderWithNav';
-import AppRouter from './router';
+import { useDispatch, useSelector } from 'react-redux';
+import AppRouter from './router/router';
+import { checkAuthThunk } from './store/auth/auth.thunk';
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { login } from './store/auth/auth.slice';
+import { initWebSocket, closeWebSocket } from './services/websocket';
+import { setWebSocketConnected } from './store/notificationCenter/notificationCenter.slice';
function App() {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const { user } = useSelector((state) => state.auth);
+ const { isConnected } = useSelector((state) => state.notificationCenter);
+
+ // 检查用户认证状态
+ useEffect(() => {
+ handleCheckAuth();
+ }, [dispatch]);
+
+ // 管理WebSocket连接
+ useEffect(() => {
+ console.log(user, isConnected);
+
+ // 如果用户已认证但WebSocket未连接,则初始化连接
+ if (user && !isConnected) {
+ // initWebSocket()
+ // .then(() => {
+ // dispatch(setWebSocketConnected(true));
+ // console.log('WebSocket connection initialized');
+ // })
+ // .catch((error) => {
+ // console.error('Failed to initialize WebSocket connection:', error);
+ // });
+ }
+
+ // 组件卸载或用户登出时关闭WebSocket连接
+ return () => {
+ if (isConnected) {
+ closeWebSocket();
+ dispatch(setWebSocketConnected(false));
+ }
+ };
+ }, [user, isConnected, dispatch]);
+
+ const handleCheckAuth = async () => {
+ console.log('app handleCheckAuth');
+ try {
+ await dispatch(checkAuthThunk()).unwrap();
+ if (!user) navigate('/login');
+ } catch (error) {
+ console.log('error', error);
+ navigate('/login');
+ }
+ };
+
return ;
}
diff --git a/src/assets/react.svg b/src/assets/react.svg
index 6c87de9..22c3eba 100644
--- a/src/assets/react.svg
+++ b/src/assets/react.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/components/AccessRequestModal.jsx b/src/components/AccessRequestModal.jsx
new file mode 100644
index 0000000..6a8654c
--- /dev/null
+++ b/src/components/AccessRequestModal.jsx
@@ -0,0 +1,232 @@
+import React, { useState } from 'react';
+import SvgIcon from './SvgIcon';
+
+/**
+ * 申请权限弹窗组件
+ * @param {Object} props
+ * @param {boolean} props.show - 是否显示弹窗
+ * @param {string} props.knowledgeBaseId - 知识库ID
+ * @param {string} props.knowledgeBaseTitle - 知识库标题
+ * @param {Function} props.onClose - 关闭弹窗的回调函数
+ * @param {Function} props.onSubmit - 提交申请的回调函数,接收 requestData 参数
+ * @param {boolean} props.isSubmitting - 是否正在提交
+ */
+export default function AccessRequestModal({
+ show,
+ knowledgeBaseId,
+ knowledgeBaseTitle,
+ onClose,
+ onSubmit,
+ isSubmitting = false,
+}) {
+ const [accessRequestData, setAccessRequestData] = useState({
+ permissions: {
+ can_read: true,
+ can_edit: false,
+ can_delete: false,
+ },
+ duration: '30', // 默认30天
+ reason: '',
+ });
+ const [accessRequestErrors, setAccessRequestErrors] = useState({});
+
+ const handleAccessRequestInputChange = (e) => {
+ const { name, value } = e.target;
+ if (name === 'duration') {
+ setAccessRequestData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ } else if (name === 'reason') {
+ setAccessRequestData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ }
+
+ // Clear error when user types
+ if (accessRequestErrors[name]) {
+ setAccessRequestErrors((prev) => ({
+ ...prev,
+ [name]: '',
+ }));
+ }
+ };
+
+ const handlePermissionChange = (permissionType) => {
+ setAccessRequestData((prev) => ({
+ ...prev,
+ permissions: {
+ can_read: true, // 只读权限始终为true
+ can_edit: permissionType === '编辑权限',
+ can_delete: false, // 管理权限暂时不开放
+ },
+ }));
+ };
+
+ const validateAccessRequestForm = () => {
+ const errors = {};
+
+ if (!accessRequestData.reason.trim()) {
+ errors.reason = '请输入申请原因';
+ }
+
+ setAccessRequestErrors(errors);
+ return Object.keys(errors).length === 0;
+ };
+
+ const handleSubmitAccessRequest = () => {
+ // Validate form
+ if (!validateAccessRequestForm()) {
+ return;
+ }
+
+ // 计算过期日期
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + parseInt(accessRequestData.duration));
+ const expiresAt = expirationDate.toISOString();
+
+ // 调用父组件的提交函数
+ onSubmit({
+ knowledge_base: knowledgeBaseId,
+ permissions: accessRequestData.permissions,
+ reason: accessRequestData.reason,
+ expires_at: expiresAt,
+ });
+
+ // 重置表单
+ setAccessRequestData({
+ permissions: {
+ can_read: true,
+ can_edit: false,
+ can_delete: false,
+ },
+ duration: '30',
+ reason: '',
+ });
+ setAccessRequestErrors({});
+ };
+
+ if (!show) return null;
+
+ return (
+
+
+
+
申请访问权限
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {accessRequestErrors.reason && (
+
{accessRequestErrors.reason}
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ApiModeSwitch.jsx b/src/components/ApiModeSwitch.jsx
new file mode 100644
index 0000000..1dbc4a6
--- /dev/null
+++ b/src/components/ApiModeSwitch.jsx
@@ -0,0 +1,83 @@
+import React, { useState, useEffect } from 'react';
+import { switchToMockApi, switchToRealApi, checkServerStatus } from '../services/api';
+
+export default function ApiModeSwitch() {
+ const [isMockMode, setIsMockMode] = useState(false);
+ const [isChecking, setIsChecking] = useState(false);
+ const [showNotification, setShowNotification] = useState(false);
+ const [notification, setNotification] = useState({ message: '', type: 'info' });
+
+ // 组件加载时检查服务器状态
+ useEffect(() => {
+ const checkStatus = async () => {
+ setIsChecking(true);
+ const isServerUp = await checkServerStatus();
+ setIsMockMode(!isServerUp);
+ setIsChecking(false);
+ };
+
+ checkStatus();
+ }, []);
+
+ // 切换API模式
+ const handleToggleMode = async () => {
+ setIsChecking(true);
+
+ if (isMockMode) {
+ // 尝试切换回真实API
+ const isServerUp = await switchToRealApi();
+ if (isServerUp) {
+ setIsMockMode(false);
+ showNotificationMessage('已切换到真实API模式', 'success');
+ } else {
+ showNotificationMessage('服务器连接失败,继续使用模拟数据', 'warning');
+ }
+ } else {
+ // 切换到模拟API
+ switchToMockApi();
+ setIsMockMode(true);
+ showNotificationMessage('已切换到模拟API模式', 'info');
+ }
+
+ setIsChecking(false);
+ };
+
+ // 显示通知消息
+ const showNotificationMessage = (message, type) => {
+ setNotification({ message, type });
+ setShowNotification(true);
+
+ // 3秒后自动隐藏通知
+ setTimeout(() => {
+ setShowNotification(false);
+ }, 3000);
+ };
+
+ return (
+
+
+
+
+
+
+ {isMockMode &&
使用本地模拟数据}
+ {!isMockMode &&
已连接到后端服务器}
+
+
+ {showNotification && (
+
+ {notification.message}
+
+ )}
+
+ );
+}
diff --git a/src/components/CreateKnowledgeBaseModal.jsx b/src/components/CreateKnowledgeBaseModal.jsx
new file mode 100644
index 0000000..9dec1a5
--- /dev/null
+++ b/src/components/CreateKnowledgeBaseModal.jsx
@@ -0,0 +1,284 @@
+import React, { useState, useEffect } from 'react';
+import SvgIcon from './SvgIcon';
+
+// 部门和组别的映射关系
+const departmentGroups = {
+ 技术部: ['开发组', '测试组', '运维组', '架构组', '安全组'],
+ 产品部: ['产品规划组', '用户研究组', '交互设计组', '项目管理组'],
+ 市场部: ['品牌推广组', '市场调研组', '客户关系组', '社交媒体组'],
+ 行政部: ['人事组', '财务组', '行政管理组', '后勤组'],
+};
+
+// 部门列表
+const departments = Object.keys(departmentGroups);
+
+/**
+ * 创建知识库模态框组件
+ * @param {Object} props
+ * @param {boolean} props.show - 是否显示弹窗
+ * @param {Object} props.formData - 表单数据
+ * @param {Object} props.formErrors - 表单错误信息
+ * @param {boolean} props.isSubmitting - 是否正在提交
+ * @param {Function} props.onClose - 关闭弹窗的回调函数
+ * @param {Function} props.onChange - 表单输入变化的回调函数
+ * @param {Function} props.onSubmit - 提交表单的回调函数
+ * @param {Object} props.currentUser - 当前用户信息
+ */
+const CreateKnowledgeBaseModal = ({
+ show,
+ formData,
+ formErrors,
+ isSubmitting,
+ onClose,
+ onChange,
+ onSubmit,
+ currentUser,
+}) => {
+ // 根据用户角色确定可以创建的知识库类型
+ const isAdmin = currentUser?.role === 'admin';
+ const isLeader = currentUser?.role === 'leader';
+
+ // 获取当前用户的部门和组别
+ const userDepartment = currentUser?.department || '';
+
+ // 可选的组别列表
+ const [availableGroups, setAvailableGroups] = useState([]);
+
+ // 当部门变化时更新可用的组别
+ useEffect(() => {
+ if (formData.department && departmentGroups[formData.department]) {
+ setAvailableGroups(departmentGroups[formData.department]);
+ } else {
+ setAvailableGroups([]);
+ }
+ }, [formData.department]);
+
+ // 提前返回需要放在所有 Hooks 之后
+ if (!show) return null;
+
+ // 获取当前用户可以创建的知识库类型
+ const getAvailableTypes = () => {
+ if (isAdmin) {
+ return [
+ { value: 'admin', label: '公共知识库' },
+ { value: 'leader', label: 'Leader 级知识库' },
+ { value: 'member', label: 'Member 级知识库' },
+ { value: 'private', label: '私有知识库' },
+ { value: 'secret', label: '保密知识库' },
+ ];
+ } else if (isLeader) {
+ return [
+ { value: 'admin', label: '公共知识库' },
+ { value: 'member', label: 'Member 级知识库' },
+ { value: 'private', label: '私有知识库' },
+ ];
+ } else {
+ return [
+ { value: 'admin', label: '公共知识库' },
+ { value: 'private', label: '私有知识库' },
+ ];
+ }
+ };
+
+ const availableTypes = getAvailableTypes();
+
+ // 判断是否需要选择组别
+ const isMemberTypeSelected = formData.type === 'member';
+ const needSelectGroup = isMemberTypeSelected;
+
+ return (
+
+
+
+
新建知识库
+
+
+
+
+
+
+ {formErrors.name &&
{formErrors.name}
}
+
+
+
+
+ {formErrors.desc &&
{formErrors.desc}
}
+
+
+
+
+ {availableTypes.map((type, index) => (
+
+
+
+
+ ))}
+
+ {!isAdmin && !isLeader && (
+
+ 您可以创建公共知识库(所有人可访问)或私有知识库(仅自己可访问)。
+
+ )}
+ {formErrors.type &&
{formErrors.type}
}
+
+
+ {/* 仅当不是私有知识库时才显示部门选项 */}
+ {formData.type !== 'private' && (
+
+
+ {isAdmin ? (
+ // 管理员可以选择任意部门
+
+ ) : (
+ // 非管理员显示只读字段
+
+ )}
+ {formErrors.department && (
+
{formErrors.department}
+ )}
+
+ )}
+
+ {/* 仅当不是私有知识库时才显示组别选项 */}
+ {formData.type !== 'private' && (
+
+
+ {isAdmin || (isLeader && needSelectGroup) ? (
+ // 管理员可以选择任意组别,组长只能选择自己部门下的组别
+
+ ) : (
+ // 普通用户显示只读字段
+
+ )}
+ {formErrors.group &&
{formErrors.group}
}
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default CreateKnowledgeBaseModal;
diff --git a/src/components/NotificationCenter.jsx b/src/components/NotificationCenter.jsx
new file mode 100644
index 0000000..dbf5372
--- /dev/null
+++ b/src/components/NotificationCenter.jsx
@@ -0,0 +1,310 @@
+import React, { useState, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import {
+ clearNotifications,
+ markAllNotificationsAsRead,
+ markNotificationAsRead,
+ setWebSocketConnected,
+} from '../store/notificationCenter/notificationCenter.slice';
+import RequestDetailSlideOver from '../pages/Permissions/components/RequestDetailSlideOver';
+import { approvePermissionThunk, rejectPermissionThunk } from '../store/permissions/permissions.thunks';
+import { showNotification } from '../store/notification.slice';
+import { initWebSocket, acknowledgeNotification, closeWebSocket } from '../services/websocket';
+
+export default function NotificationCenter({ show, onClose }) {
+ const [showAll, setShowAll] = useState(false);
+ const dispatch = useDispatch();
+ const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter);
+ const [selectedRequest, setSelectedRequest] = useState(null);
+ const [showSlideOver, setShowSlideOver] = useState(false);
+ const [showResponseInput, setShowResponseInput] = useState(false);
+ const [currentRequestId, setCurrentRequestId] = useState(null);
+ const [isApproving, setIsApproving] = useState(false);
+ const [responseMessage, setResponseMessage] = useState('');
+ const { isAuthenticated } = useSelector((state) => state.auth);
+
+ const displayedNotifications = showAll ? notifications : notifications.slice(0, 5);
+
+ // 初始化WebSocket连接
+ useEffect(() => {
+ // 只有在用户已登录的情况下才连接WebSocket
+ if (isAuthenticated && !isConnected) {
+ initWebSocket()
+ .then(() => {
+ dispatch(setWebSocketConnected(true));
+ console.log('Successfully connected to notification WebSocket');
+ })
+ .catch((error) => {
+ console.error('Failed to connect to notification WebSocket:', error);
+ dispatch(setWebSocketConnected(false));
+ // 可以在这里显示连接失败的通知
+ dispatch(
+ showNotification({
+ message: '通知服务连接失败,部分功能可能不可用',
+ type: 'warning',
+ })
+ );
+ });
+ }
+
+ // 组件卸载时关闭WebSocket连接
+ return () => {
+ if (isConnected) {
+ closeWebSocket();
+ dispatch(setWebSocketConnected(false));
+ }
+ };
+ }, [isAuthenticated, isConnected, dispatch]);
+
+ const handleClearAll = () => {
+ dispatch(clearNotifications());
+ };
+
+ const handleMarkAllAsRead = () => {
+ dispatch(markAllNotificationsAsRead());
+ };
+
+ const handleMarkAsRead = (notificationId) => {
+ dispatch(markNotificationAsRead(notificationId));
+ // 同时发送确认消息到服务器
+ acknowledgeNotification(notificationId);
+ };
+
+ const handleViewDetail = (notification) => {
+ // 标记为已读
+ if (!notification.isRead) {
+ handleMarkAsRead(notification.id);
+ }
+
+ if (notification.type === 'permission') {
+ setSelectedRequest(notification);
+ setShowSlideOver(true);
+ }
+ };
+
+ const handleCloseSlideOver = () => {
+ setShowSlideOver(false);
+ setTimeout(() => {
+ setSelectedRequest(null);
+ }, 300);
+ };
+
+ const handleOpenResponseInput = (requestId, approving) => {
+ setCurrentRequestId(requestId);
+ setIsApproving(approving);
+ setShowResponseInput(true);
+ };
+
+ const handleCloseResponseInput = () => {
+ setShowResponseInput(false);
+ setCurrentRequestId(null);
+ setResponseMessage('');
+ };
+
+ const handleProcessRequest = () => {
+ if (!currentRequestId) return;
+
+ const params = {
+ id: currentRequestId,
+ responseMessage,
+ };
+
+ if (isApproving) {
+ dispatch(approvePermissionThunk(params));
+ } else {
+ dispatch(rejectPermissionThunk(params));
+ }
+ };
+
+ if (!show) return null;
+
+ return (
+ <>
+
+
+
+
通知中心
+ {unreadCount > 0 && {unreadCount}}
+ {isConnected ? (
+ 已连接
+ ) : (
+ 未连接
+ )}
+
+
+
+
+
+
+
+
+ {displayedNotifications.length === 0 ? (
+
+ ) : (
+ displayedNotifications.map((notification) => (
+
+
+
+
+
+
+
+
+ {notification.title}
+
+ {notification.time}
+
+
{notification.content}
+
+ {notification.hasDetail && (
+
+ )}
+ {!notification.isRead && (
+
+ )}
+
+
+
+
+ ))
+ )}
+
+ {notifications.length > 5 && (
+
+
+
+ )}
+
+
+ {/* 使用滑动面板组件 */}
+ handleOpenResponseInput(id, true)}
+ onReject={(id) => handleOpenResponseInput(id, false)}
+ processingId={currentRequestId}
+ approveRejectStatus={showResponseInput ? 'loading' : 'idle'}
+ isApproving={isApproving}
+ />
+
+ {/* 回复输入弹窗 */}
+ {showResponseInput && (
+
+
+
+
+
{isApproving ? '批准' : '拒绝'}申请
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/Pagination.jsx b/src/components/Pagination.jsx
new file mode 100644
index 0000000..e16ab79
--- /dev/null
+++ b/src/components/Pagination.jsx
@@ -0,0 +1,59 @@
+import React from 'react';
+
+/**
+ * 分页组件
+ * @param {Object} props
+ * @param {number} props.currentPage - 当前页码
+ * @param {number} props.totalPages - 总页数
+ * @param {number} props.pageSize - 每页显示的条目数
+ * @param {Function} props.onPageChange - 页码变化的回调函数
+ * @param {Function} props.onPageSizeChange - 每页条目数变化的回调函数
+ */
+const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default Pagination;
diff --git a/src/components/SearchBar.jsx b/src/components/SearchBar.jsx
new file mode 100644
index 0000000..c6ae245
--- /dev/null
+++ b/src/components/SearchBar.jsx
@@ -0,0 +1,193 @@
+import React, { useRef, useState, useEffect } from 'react';
+import SvgIcon from './SvgIcon';
+
+/**
+ * 搜索栏组件
+ * @param {Object} props
+ * @param {string} props.searchKeyword - 搜索关键词
+ * @param {boolean} props.isSearching - 是否正在搜索
+ * @param {Function} props.onSearchChange - 搜索关键词变化的回调函数
+ * @param {Function} props.onSearch - 提交搜索的回调函数
+ * @param {Function} props.onClearSearch - 清除搜索的回调函数
+ * @param {string} props.placeholder - 搜索框占位文本
+ * @param {string} props.className - 额外的 CSS 类名
+ * @param {Array} props.searchResults - 搜索结果
+ * @param {boolean} props.isSearchLoading - 搜索是否正在加载
+ * @param {Function} props.onResultClick - 点击搜索结果的回调
+ * @param {Function} props.onRequestAccess - 申请权限的回调
+ */
+const SearchBar = ({
+ searchKeyword,
+ isSearching,
+ onSearchChange,
+ onSearch,
+ onClearSearch,
+ placeholder = '搜索...',
+ className = 'w-50',
+ searchResults = [],
+ isSearchLoading = false,
+ onResultClick,
+ onRequestAccess,
+}) => {
+ const [showDropdown, setShowDropdown] = useState(false);
+ const searchRef = useRef(null);
+ const inputRef = useRef(null);
+
+ // 处理点击外部关闭下拉框
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (searchRef.current && !searchRef.current.contains(event.target)) {
+ setShowDropdown(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ // 只有在用户执行搜索后且有结果时显示下拉框
+ useEffect(() => {
+ if (isSearching && searchResults.length > 0) {
+ setShowDropdown(true);
+ }
+ }, [isSearching, searchResults]);
+
+ // 处理输入变化
+ const handleInputChange = (e) => {
+ onSearchChange(e);
+ };
+
+ // 处理搜索提交
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSearch(e);
+ // 搜索提交后,如果有关键词,将显示下拉框(由searchResults决定)
+ };
+
+ return (
+
+
+
+ {/* 搜索结果下拉框 - 仅在用户搜索且有结果时显示 */}
+ {showDropdown && (isSearchLoading || searchResults?.length > 0) && (
+
+
+ {isSearchLoading ? (
+
+ ) : searchResults?.length > 0 ? (
+ <>
+
+ 搜索结果 ({searchResults.length})
+
+ {searchResults.map((item) => (
+
+
+
{
+ if (item.permissions?.can_read) {
+ onResultClick(item.id, item.permissions);
+ setShowDropdown(false);
+ }
+ }}
+ >
+
+
+
+ {item.highlighted_name ? (
+
+ ) : (
+ item.name
+ )}
+
+
+
+
+ {item.type === 'private' ? '私有' : item.type}
+
+ {item.department && {item.department}}
+ {!item.permissions?.can_read && (
+
+
+ 无权限
+
+ )}
+
+
+ {!item.permissions?.can_read && (
+
+ )}
+
+
+ ))}
+ >
+ ) : (
+
未找到匹配的知识库
+ )}
+
+
+ )}
+
+ );
+};
+
+export default SearchBar;
diff --git a/src/components/Snackbar.jsx b/src/components/Snackbar.jsx
index 1b84c48..8930889 100644
--- a/src/components/Snackbar.jsx
+++ b/src/components/Snackbar.jsx
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
+import SvgIcon from './SvgIcon';
const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
if (!message) return null;
@@ -10,7 +11,7 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
}, duration);
return () => clearTimeout(timer);
}
- }, [duration, onClose]);
+ }, [message, duration, onClose]);
const icons = {
success: 'check-circle-fill',
@@ -19,43 +20,21 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
danger: 'exclamation-triangle-fill',
};
- return (
- <>
-
+ // 处理关闭按钮点击
+ const handleClose = (e) => {
+ e.preventDefault();
+ if (onClose) onClose();
+ };
-
- >
+ return (
+
);
};
diff --git a/src/components/SvgIcon.jsx b/src/components/SvgIcon.jsx
new file mode 100644
index 0000000..61d2b0c
--- /dev/null
+++ b/src/components/SvgIcon.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { icons } from '../icons/icons';
+
+export default function SvgIcon({ className, width, height, color, style }) {
+ // Create a new SVG string with custom attributes if provided
+ const customizeSvg = (svgString) => {
+ if (!svgString) return '';
+
+ // If no customization needed, return the original SVG
+ if (!width && !height && !color) return svgString;
+
+ // Parse the SVG to modify attributes
+ let modifiedSvg = svgString;
+
+ // Replace width if provided
+ if (width) {
+ modifiedSvg = modifiedSvg.replace(/width=['"]([^'"]*)['"]/g, `width="${width}"`);
+ }
+
+ // Replace height if provided
+ if (height) {
+ modifiedSvg = modifiedSvg.replace(/height=['"]([^'"]*)['"]/g, `height="${height}"`);
+ }
+
+ // Replace fill color if provided
+ if (color) {
+ modifiedSvg = modifiedSvg.replace(/fill=['"]currentColor['"]/g, `fill="${color}"`);
+ }
+
+ return modifiedSvg;
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/UserSettingsModal.jsx b/src/components/UserSettingsModal.jsx
new file mode 100644
index 0000000..73a0828
--- /dev/null
+++ b/src/components/UserSettingsModal.jsx
@@ -0,0 +1,101 @@
+import React, { useState } from 'react';
+import { useSelector } from 'react-redux';
+import '../styles/style.scss';
+
+export default function UserSettingsModal({ show, onClose }) {
+ const { user } = useSelector((state) => state.auth);
+ const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
+
+ if (!show) return null;
+
+ return (
+
+
+
+
+
管理员个人设置
+
+
+
+
+
个人信息
+
+
+
+
+
+
+
+
+
+
+
+
安全设置
+
+
+
+
+ 修改密码
+
+
上次修改:{lastPasswordChange}
+
+
+
+
+
+
+
+
通知设置
+
+
+
+
新的数据集访问申请通知
+
+
+
+
+
异常登录和权限变更提醒
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/icons/icons.js b/src/icons/icons.js
new file mode 100644
index 0000000..364ba22
--- /dev/null
+++ b/src/icons/icons.js
@@ -0,0 +1,123 @@
+export const icons = {
+ plus: ``,
+ 'more-dot': ``,
+ trash: ``,
+ 'check-circle-fill': ``,
+ 'info-fill': ``,
+ 'exclamation-triangle-fill': ``,
+ file: ``,
+ clock: ``,
+ 'circle-yes': ``,
+ eye: ``,
+ 'chat-dot': ``,
+ key: ``,
+ lock: ``,
+ 'stack-fill': ``,
+ edit: ``,
+ list: ``,
+ 'setting-fill': ``,
+ dataset: ``,
+ calendar: ``,
+ clipboard: ``,
+ chat: ``,
+ 'arrowup-upload': ``,
+ send: ``,
+ search: ``,
+ bell: ``,
+ 'magnifying-glass': ``,
+ close: ``,
+ 'knowledge-base': ``,
+ 'knowledge-base-large': ``,
+ plus: ``,
+};
diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx
index 835935f..a843cf0 100644
--- a/src/layouts/HeaderWithNav.jsx
+++ b/src/layouts/HeaderWithNav.jsx
@@ -1,84 +1,170 @@
-import React from 'react';
+import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { Link, useNavigate, useLocation } from 'react-router-dom';
+import { logoutThunk } from '../store/auth/auth.thunk';
+import UserSettingsModal from '../components/UserSettingsModal';
+import NotificationCenter from '../components/NotificationCenter';
+import SvgIcon from '../components/SvgIcon';
export default function HeaderWithNav() {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { user } = useSelector((state) => state.auth);
+ const [showSettings, setShowSettings] = useState(false);
+ const [showNotifications, setShowNotifications] = useState(false);
+ const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter);
+
+ const handleLogout = async () => {
+ try {
+ await dispatch(logoutThunk()).unwrap();
+ sessionStorage.removeItem('token');
+ navigate('/login');
+ } catch (error) {}
+ };
+
+ // Check if the current path starts with the given path
+ const isActive = (path) => {
+ return location.pathname.startsWith(path);
+ };
+ console.log('user', user);
+
+ // 检查用户是否有管理权限(leader 或 admin)
+ const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
+
return (
-
-
-
+
+
setShowSettings(false)} />
+ setShowNotifications(false)} />
);
}
diff --git a/src/layouts/Mainlayout.jsx b/src/layouts/Mainlayout.jsx
index 341d730..76ef448 100644
--- a/src/layouts/Mainlayout.jsx
+++ b/src/layouts/Mainlayout.jsx
@@ -7,7 +7,6 @@ export default function Mainlayout({ children }) {
return (
<>
-
{children}
>
);
diff --git a/src/main.jsx b/src/main.jsx
index dbe6730..8203d0a 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -10,7 +10,7 @@ import { PersistGate } from 'redux-persist/integration/react';
import Loading from './components/Loading.jsx';
createRoot(document.getElementById('root')).render(
-
+ //
} persistor={persistor}>
@@ -18,5 +18,5 @@ createRoot(document.getElementById('root')).render(
-
+ //
);
diff --git a/src/pages/Chat/Chat.jsx b/src/pages/Chat/Chat.jsx
new file mode 100644
index 0000000..feed2d0
--- /dev/null
+++ b/src/pages/Chat/Chat.jsx
@@ -0,0 +1,158 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchChats, deleteChat, createChatRecord } from '../../store/chat/chat.thunks';
+import { showNotification } from '../../store/notification.slice';
+import ChatSidebar from './ChatSidebar';
+import NewChat from './NewChat';
+import ChatWindow from './ChatWindow';
+
+export default function Chat() {
+ const { knowledgeBaseId, chatId } = useParams();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ // 从 Redux store 获取聊天记录列表
+ const {
+ items: chatHistory,
+ status,
+ error,
+ } = useSelector((state) => state.chat.history || { items: [], status: 'idle', error: null });
+ const operationStatus = useSelector((state) => state.chat.createSession?.status);
+ const operationError = useSelector((state) => state.chat.createSession?.error);
+
+ // 获取聊天记录列表
+ useEffect(() => {
+ dispatch(fetchChats({ page: 1, page_size: 20 }));
+ }, [dispatch]);
+
+ // 监听操作状态,显示通知
+ useEffect(() => {
+ if (operationStatus === 'succeeded') {
+ dispatch(
+ showNotification({
+ message: '操作成功',
+ type: 'success',
+ })
+ );
+ } else if (operationStatus === 'failed' && operationError) {
+ dispatch(
+ showNotification({
+ message: `操作失败: ${operationError}`,
+ type: 'danger',
+ })
+ );
+ }
+ }, [operationStatus, operationError, dispatch]);
+
+ // If we have a knowledgeBaseId but no chatId, check if we have an existing chat or create a new one
+ useEffect(() => {
+ if (knowledgeBaseId && !chatId) {
+ // 检查是否存在包含此知识库的聊天记录
+ const existingChat = chatHistory.find((chat) => {
+ // 检查知识库ID是否匹配
+ if (chat.datasets && Array.isArray(chat.datasets)) {
+ return chat.datasets.some((ds) => ds.id === knowledgeBaseId);
+ }
+ // 兼容旧格式
+ if (chat.dataset_id_list && Array.isArray(chat.dataset_id_list)) {
+ return chat.dataset_id_list.includes(knowledgeBaseId.replace(/-/g, ''));
+ }
+ return false;
+ });
+ console.log('existingChat', existingChat);
+
+ if (existingChat) {
+ // 找到现有聊天记录,导航到该聊天页面
+ navigate(`/chat/${knowledgeBaseId}/${existingChat.conversation_id}`);
+ } else {
+ // 创建新聊天
+ dispatch(
+ createChatRecord({
+ dataset_id_list: [knowledgeBaseId.replace(/-/g, '')],
+ question: '选择当前知识库,创建聊天',
+ })
+ )
+ .unwrap()
+ .then((response) => {
+ // 创建成功,使用返回的conversation_id导航
+ if (response && response.conversation_id) {
+ navigate(`/chat/${knowledgeBaseId}/${response.conversation_id}`);
+ } else {
+ // 错误处理
+ dispatch(
+ showNotification({
+ message: '创建聊天失败:未能获取会话ID',
+ type: 'danger',
+ })
+ );
+ }
+ })
+ .catch((error) => {
+ dispatch(
+ showNotification({
+ message: `创建聊天失败: ${error}`,
+ type: 'danger',
+ })
+ );
+ });
+ }
+ }
+ }, [knowledgeBaseId, chatId, chatHistory, navigate, dispatch]);
+
+ const handleDeleteChat = (id) => {
+ // 调用 Redux action 删除聊天
+ dispatch(deleteChat(id))
+ .unwrap()
+ .then(() => {
+ // 删除成功后显示通知
+ dispatch(
+ showNotification({
+ message: '聊天记录已删除',
+ type: 'success',
+ })
+ );
+
+ // If the deleted chat is the current one, navigate to the chat list
+ if (chatId === id) {
+ navigate('/chat');
+ }
+ })
+ .catch((error) => {
+ // 删除失败显示错误通知
+ dispatch(
+ showNotification({
+ message: `删除失败: ${error}`,
+ type: 'danger',
+ })
+ );
+ });
+ };
+
+ return (
+
+
+ {/* Sidebar */}
+
+
+
+
+ {/* Main Content */}
+
+ {!chatId ? : }
+
+
+
+ );
+}
diff --git a/src/pages/Chat/ChatSidebar.jsx b/src/pages/Chat/ChatSidebar.jsx
new file mode 100644
index 0000000..5e5bdba
--- /dev/null
+++ b/src/pages/Chat/ChatSidebar.jsx
@@ -0,0 +1,143 @@
+import React, { useState } from 'react';
+import { Link, useNavigate, useParams } from 'react-router-dom';
+import SvgIcon from '../../components/SvgIcon';
+
+export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading = false, hasError = false }) {
+ const navigate = useNavigate();
+ const { chatId, knowledgeBaseId } = useParams();
+ const [activeDropdown, setActiveDropdown] = useState(null);
+
+ const handleNewChat = () => {
+ navigate('/chat');
+ };
+
+ const handleMouseEnter = (id) => {
+ setActiveDropdown(id);
+ };
+
+ const handleMouseLeave = () => {
+ setActiveDropdown(null);
+ };
+
+ const handleDeleteChat = (e, id) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (onDeleteChat) {
+ onDeleteChat(id);
+ }
+ setActiveDropdown(null);
+ };
+
+ // 渲染加载状态
+ const renderLoading = () => (
+
+
+ 加载中...
+
+
加载聊天记录...
+
+ );
+
+ // 渲染错误状态
+ const renderError = () => (
+
+ );
+
+ // 渲染空状态
+ const renderEmpty = () => (
+
+ );
+
+ return (
+
+
+
聊天记录
+
+
+
+
+
+
+
+ {isLoading ? (
+ renderLoading()
+ ) : hasError ? (
+ renderError()
+ ) : chatHistory.length === 0 ? (
+ renderEmpty()
+ ) : (
+
+ {chatHistory.map((chat) => (
+ -
+
+
+
+ {chat.datasets?.map((ds) => ds.name).join(', ') || '未命名知识库'}
+
+
+ {chat.last_message
+ ? chat.last_message.length > 30
+ ? chat.last_message.substring(0, 30) + '...'
+ : chat.last_message
+ : '新对话'}
+
+
+ {chat.last_time && new Date(chat.last_time).toLocaleDateString()}
+
+
+
+ handleMouseEnter(chat.conversation_id)}
+ onMouseLeave={handleMouseLeave}
+ >
+
+ {activeDropdown === chat.conversation_id && (
+
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/Chat/ChatWindow.jsx b/src/pages/Chat/ChatWindow.jsx
new file mode 100644
index 0000000..e80659c
--- /dev/null
+++ b/src/pages/Chat/ChatWindow.jsx
@@ -0,0 +1,297 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchMessages } from '../../store/chat/chat.messages.thunks';
+import { resetMessages, resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice';
+import { showNotification } from '../../store/notification.slice';
+import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks';
+import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
+import SvgIcon from '../../components/SvgIcon';
+import { get } from '../../services/api';
+
+export default function ChatWindow({ chatId, knowledgeBaseId }) {
+ const dispatch = useDispatch();
+ const [inputMessage, setInputMessage] = useState('');
+ const [loading, setLoading] = useState(false);
+ const messagesEndRef = useRef(null);
+
+ // 从 Redux store 获取消息
+ const messages = useSelector((state) => state.chat.messages.items);
+ const messageStatus = useSelector((state) => state.chat.messages.status);
+ const messageError = useSelector((state) => state.chat.messages.error);
+ const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
+
+ // 使用新的Redux状态结构
+ const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
+ const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId);
+ const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.loading);
+
+ // 获取可用数据集列表
+ const availableDatasets = useSelector((state) => state.chat.availableDatasets.items || []);
+ const availableDatasetsLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
+
+ // 获取会话详情
+ const conversation = useSelector((state) => state.chat.currentChat.data);
+ const conversationStatus = useSelector((state) => state.chat.currentChat.status);
+ const conversationError = useSelector((state) => state.chat.currentChat.error);
+
+ // 获取聊天详情
+ useEffect(() => {
+ if (chatId) {
+ setLoading(true);
+ dispatch(fetchConversationDetail(chatId))
+ .unwrap()
+ .catch((error) => {
+ // 如果是新聊天,API会返回404,此时不显示错误
+ if (error && error !== 'Error: Request failed with status code 404') {
+ dispatch(
+ showNotification({
+ message: `获取聊天详情失败: ${error || '未知错误'}`,
+ type: 'danger',
+ })
+ );
+ }
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }
+
+ // 组件卸载时清空消息
+ return () => {
+ dispatch(resetMessages());
+ };
+ }, [chatId, dispatch]);
+
+ // 新会话自动添加欢迎消息
+ useEffect(() => {
+ // 如果是新聊天且没有任何消息,添加一条系统欢迎消息
+ if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') {
+ const selectedKb = knowledgeBase ||
+ availableDatasets.find((ds) => ds.id === knowledgeBaseId) || { name: '知识库' };
+
+ dispatch(
+ addMessage({
+ id: 'welcome-' + Date.now(),
+ role: 'assistant',
+ content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`,
+ created_at: new Date().toISOString(),
+ })
+ );
+ }
+ }, [chatId, messages.length, loading, messageStatus, knowledgeBase, knowledgeBaseId, availableDatasets, dispatch]);
+
+ // 监听发送消息状态
+ useEffect(() => {
+ if (sendStatus === 'failed' && sendError) {
+ dispatch(
+ showNotification({
+ message: `发送失败: ${sendError}`,
+ type: 'danger',
+ })
+ );
+ dispatch(resetSendMessageStatus());
+ }
+ }, [sendStatus, sendError, dispatch]);
+
+ // 滚动到底部
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ // 获取当前会话的知识库信息
+ useEffect(() => {
+ // 如果conversation有数据集信息,优先使用它
+ if (conversation && conversation.datasets && conversation.datasets.length > 0) {
+ return;
+ }
+
+ // 如果没有会话数据集信息,但有knowledgeBaseId,尝试从知识库列表中查找
+ if (knowledgeBaseId && knowledgeBases.length === 0 && !availableDatasets.length) {
+ dispatch(fetchAvailableDatasets());
+ }
+ }, [dispatch, knowledgeBaseId, knowledgeBases, conversation, availableDatasets]);
+
+ const handleSendMessage = (e) => {
+ e.preventDefault();
+
+ if (!inputMessage.trim() || sendStatus === 'loading') return;
+
+ // 获取知识库ID列表
+ let dataset_id_list = [];
+
+ if (conversation && conversation.datasets) {
+ // 如果已有会话,使用会话中的知识库
+ dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, ''));
+ } else if (knowledgeBaseId) {
+ // 如果是新会话,使用当前选择的知识库
+ dataset_id_list = [knowledgeBaseId.replace(/-/g, '')];
+ } else if (availableDatasets.length > 0) {
+ // 如果都没有,尝试使用可用知识库列表中的第一个
+ dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')];
+ }
+
+ if (dataset_id_list.length === 0) {
+ dispatch(
+ showNotification({
+ message: '发送失败:未选择知识库',
+ type: 'danger',
+ })
+ );
+ return;
+ }
+
+ // 发送消息到服务器
+ dispatch(
+ createChatRecord({
+ dataset_id_list: dataset_id_list,
+ question: inputMessage,
+ conversation_id: chatId,
+ })
+ )
+ .unwrap()
+ .then(() => {
+ // 成功发送后,可以执行任何需要的操作
+ // 例如:在用户发送第一条消息后更新URL中的会话ID
+ })
+ .catch((error) => {
+ // 发送失败,显示错误信息
+ dispatch(
+ showNotification({
+ message: `发送失败: ${error}`,
+ type: 'danger',
+ })
+ );
+ });
+
+ // 清空输入框
+ setInputMessage('');
+ };
+
+ // 渲染加载状态
+ const renderLoading = () => (
+
+
+ 加载中...
+
+
加载聊天记录...
+
+ );
+
+ // 渲染错误状态
+ const renderError = () => (
+
+
+ 加载消息失败
+
+
{messageError}
+
+
+ );
+
+ // 渲染空消息状态
+ const renderEmpty = () => {
+ if (loading) return null;
+
+ return (
+
+ );
+ };
+
+ return (
+
+ {/* Chat header */}
+
+ {conversation && conversation.datasets ? (
+ <>
+
{conversation.datasets.map((dataset) => dataset.name).join(', ')}
+ {conversation.datasets.length > 0 && conversation.datasets[0].type && (
+ 类型: {conversation.datasets[0].type}
+ )}
+ >
+ ) : knowledgeBase ? (
+ <>
+ {knowledgeBase.name}
+ {knowledgeBase.description}
+ >
+ ) : (
+ {loading || availableDatasetsLoading ? '加载中...' : '聊天'}
+ )}
+
+
+ {/* Chat messages */}
+
+
+ {messageStatus === 'loading'
+ ? renderLoading()
+ : messageStatus === 'failed'
+ ? renderError()
+ : messages.length === 0
+ ? renderEmpty()
+ : messages.map((message) => (
+
+
+
+ {message.created_at && new Date(message.created_at).toLocaleTimeString()}
+
+
+ ))}
+
+ {sendStatus === 'loading' && (
+
+ )}
+
+
+
+
+
+ {/* Chat input */}
+
+
+
+
+ );
+}
diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx
new file mode 100644
index 0000000..bf81e5b
--- /dev/null
+++ b/src/pages/Chat/NewChat.jsx
@@ -0,0 +1,112 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { showNotification } from '../../store/notification.slice';
+import { fetchAvailableDatasets, fetchChats, createChatRecord } from '../../store/chat/chat.thunks';
+import SvgIcon from '../../components/SvgIcon';
+
+export default function NewChat() {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ // 从 Redux store 获取可用知识库数据
+ const datasets = useSelector((state) => state.chat.availableDatasets.items || []);
+ const isLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
+ const error = useSelector((state) => state.chat.availableDatasets.error);
+
+ // 获取聊天历史记录
+ const chatHistory = useSelector((state) => state.chat.history.items || []);
+ const chatHistoryLoading = useSelector((state) => state.chat.history.status === 'loading');
+
+ // 获取可用知识库列表和聊天历史
+ useEffect(() => {
+ dispatch(fetchAvailableDatasets());
+ dispatch(fetchChats({ page: 1, page_size: 50 }));
+ }, [dispatch]);
+
+ // 监听错误状态
+ useEffect(() => {
+ if (error) {
+ dispatch(
+ showNotification({
+ message: `获取可用知识库列表失败: ${error}`,
+ type: 'danger',
+ })
+ );
+ }
+ }, [error, dispatch]);
+
+ // 处理知识库选择
+ const handleSelectKnowledgeBase = (dataset) => {
+ // 判断聊天历史中是否已存在该知识库的聊天记录
+ const existingChat = chatHistory.find((chat) => {
+ // 检查知识库ID是否匹配
+ if (chat.datasets && Array.isArray(chat.datasets)) {
+ return chat.datasets.some((ds) => ds.id === dataset.id);
+ }
+ // 兼容旧格式
+ if (chat.dataset_id_list && Array.isArray(chat.dataset_id_list)) {
+ return chat.dataset_id_list.includes(dataset.id.replace(/-/g, ''));
+ }
+ return false;
+ });
+
+ if (existingChat) {
+ // 找到现有聊天记录,导航到该聊天页面
+ navigate(`/chat/${dataset.id}/${existingChat.conversation_id}`);
+ } else {
+ // 没有找到现有聊天记录,创建新的聊天
+ navigate(`/chat/${dataset.id}`);
+ }
+ };
+
+ // 渲染加载状态
+ if (isLoading || chatHistoryLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
选择知识库开始聊天
+
+ {datasets.length > 0 ? (
+ datasets.map((dataset) => (
+
+
handleSelectKnowledgeBase(dataset)}
+ >
+
+
{dataset.name}
+
{dataset.desc || dataset.description || ''}
+
+
+
+ {dataset.document_count || 0} 文档
+
+
+
+ {dataset.create_time
+ ? new Date(dataset.create_time).toLocaleDateString()
+ : 'N/A'}
+
+
+
+
+
+ ))
+ ) : (
+
+
暂无可访问的知识库,请先申请知识库访问权限
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx
new file mode 100644
index 0000000..ae4b1e8
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx
@@ -0,0 +1,374 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useDispatch } from 'react-redux';
+import { showNotification } from '../../../store/notification.slice';
+import SvgIcon from '../../../components/SvgIcon';
+
+// 导入拆分的组件
+import Breadcrumb from './components/Breadcrumb';
+import DocumentList from './components/DocumentList';
+import FileUploadModal from './components/FileUploadModal';
+
+export default function DatasetTab({ knowledgeBase }) {
+ const dispatch = useDispatch();
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedDocuments, setSelectedDocuments] = useState([]);
+ const [selectAll, setSelectAll] = useState(false);
+ const [showBatchDropdown, setShowBatchDropdown] = useState(false);
+ const [showAddFileModal, setShowAddFileModal] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [newFile, setNewFile] = useState({
+ name: '',
+ description: '',
+ file: null,
+ });
+ const [fileErrors, setFileErrors] = useState({});
+ const dropdownRef = useRef(null);
+ const fileInputRef = useRef(null);
+
+ // Use documents from knowledge base or empty array if not available
+ const [documents, setDocuments] = useState(knowledgeBase.documents || []);
+
+ // Update documents when knowledgeBase changes
+ useEffect(() => {
+ setDocuments(knowledgeBase.documents || []);
+ }, [knowledgeBase]);
+
+ // Handle click outside dropdown
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setShowBatchDropdown(false);
+ }
+ }
+
+ // Add event listener
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ // Remove event listener on cleanup
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [dropdownRef]);
+
+ // Handle search input change
+ const handleSearchChange = (e) => {
+ setSearchQuery(e.target.value);
+ };
+
+ // Handle select all checkbox
+ const handleSelectAll = () => {
+ if (selectAll) {
+ setSelectedDocuments([]);
+ } else {
+ setSelectedDocuments(documents.map((doc) => doc.id));
+ }
+ setSelectAll(!selectAll);
+ };
+
+ // Handle individual document selection
+ const handleSelectDocument = (docId) => {
+ if (selectedDocuments.includes(docId)) {
+ setSelectedDocuments(selectedDocuments.filter((id) => id !== docId));
+ setSelectAll(false);
+ } else {
+ setSelectedDocuments([...selectedDocuments, docId]);
+ if (selectedDocuments.length + 1 === documents.length) {
+ setSelectAll(true);
+ }
+ }
+ };
+
+ // Handle batch delete
+ const handleBatchDelete = () => {
+ if (selectedDocuments.length === 0) return;
+
+ // Here you would typically call an API to delete the selected documents
+ console.log('Deleting documents:', selectedDocuments);
+
+ // Update documents state by removing selected documents
+ setDocuments((prevDocuments) => prevDocuments.filter((doc) => !selectedDocuments.includes(doc.id)));
+
+ // Show notification
+ dispatch(
+ showNotification({
+ message: '已删除选中的数据集',
+ type: 'success',
+ })
+ );
+
+ // Reset selection
+ setSelectedDocuments([]);
+ setSelectAll(false);
+ setShowBatchDropdown(false);
+ };
+
+ // Handle file input change
+ const handleFileChange = (e) => {
+ const selectedFile = e.target.files[0];
+ if (selectedFile) {
+ setNewFile({
+ ...newFile,
+ name: selectedFile.name,
+ file: selectedFile,
+ });
+
+ // Clear file error if exists
+ if (fileErrors.file) {
+ setFileErrors((prev) => ({
+ ...prev,
+ file: '',
+ }));
+ }
+ }
+ };
+
+ // Handle description input change
+ const handleDescriptionChange = (e) => {
+ setNewFile({
+ ...newFile,
+ description: e.target.value,
+ });
+ };
+
+ // Handle file drop
+ const handleFileDrop = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const droppedFile = e.dataTransfer.files[0];
+ if (droppedFile) {
+ setNewFile({
+ ...newFile,
+ name: droppedFile.name,
+ file: droppedFile,
+ });
+
+ // Clear file error if exists
+ if (fileErrors.file) {
+ setFileErrors((prev) => ({
+ ...prev,
+ file: '',
+ }));
+ }
+ }
+ };
+
+ // Prevent default behavior for drag events
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ // Validate file form
+ const validateFileForm = () => {
+ const errors = {};
+
+ if (!newFile.file) {
+ errors.file = '请上传文件';
+ }
+
+ setFileErrors(errors);
+ return Object.keys(errors).length === 0;
+ };
+
+ // Handle file upload
+ const handleFileUpload = () => {
+ // Validate form
+ if (!validateFileForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ // Here you would typically call an API to upload the file
+ console.log('Uploading file:', newFile);
+
+ // Simulate API call
+ setTimeout(() => {
+ // Generate a new ID for the document
+ const newId = Date.now().toString();
+
+ // Format file size
+ const fileSizeKB = newFile.file ? (newFile.file.size / 1024).toFixed(0) + 'kb' : '0kb';
+
+ // Get current date
+ const today = new Date();
+ const formattedDate = today.toISOString();
+
+ // Create new document object
+ const newDocument = {
+ id: newId,
+ name: newFile.name,
+ description: newFile.description || '无描述',
+ size: fileSizeKB,
+ create_time: formattedDate,
+ update_time: formattedDate,
+ };
+
+ // Add new document to the documents array
+ setDocuments((prevDocuments) => [...prevDocuments, newDocument]);
+
+ // Show notification
+ dispatch(
+ showNotification({
+ message: '数据集上传成功',
+ type: 'success',
+ })
+ );
+
+ setIsSubmitting(false);
+ // Reset form and close modal
+ handleCloseAddFileModal();
+ }, 1000);
+ };
+
+ // Open file selector when clicking on the upload area
+ const handleUploadAreaClick = () => {
+ fileInputRef.current.click();
+ };
+
+ // Handle close modal
+ const handleCloseAddFileModal = () => {
+ setNewFile({
+ name: '',
+ description: '',
+ file: null,
+ });
+ setFileErrors({});
+ setShowAddFileModal(false);
+ };
+
+ // Handle delete document
+ const handleDeleteDocument = (docId) => {
+ // Here you would typically call an API to delete the document
+ console.log('Deleting document:', docId);
+
+ // Update documents state by removing the deleted document
+ setDocuments((prevDocuments) => prevDocuments.filter((doc) => doc.id !== docId));
+
+ // Show notification
+ dispatch(
+ showNotification({
+ message: '数据集已删除',
+ type: 'success',
+ })
+ );
+ };
+
+ // Filter documents based on search query
+ const filteredDocuments = documents.filter(
+ (doc) =>
+ doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (doc.description && doc.description.toLowerCase().includes(searchQuery.toLowerCase()))
+ );
+
+ return (
+ <>
+ {/* Breadcrumb navigation */}
+
+
+ {/* Toolbar */}
+
+
+
+ {selectedDocuments.length > 0 && (
+
+
+ {showBatchDropdown && (
+
+ )}
+
+ )}
+
+
+
+
+
+
+ {/* Document list */}
+
+ {/* Pagination */}
+
+
+ 每页行数:
+
+
+
+
1-5 of 10
+
+
+
+
+ {/* File upload modal */}
+
+ >
+ );
+}
diff --git a/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx b/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx
new file mode 100644
index 0000000..a986f25
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx
@@ -0,0 +1,119 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useSelector, useDispatch } from 'react-redux';
+import { showNotification } from '../../../store/notification.slice';
+import { getKnowledgeBaseById } from '../../../store/knowledgeBase/knowledgeBase.thunks';
+import SvgIcon from '../../../components/SvgIcon';
+import DatasetTab from './DatasetTab';
+import SettingsTab from './SettingsTab';
+
+export default function KnowledgeBaseDetail() {
+ const { id, tab } = useParams();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets');
+
+ // Get knowledge base details from Redux store - 使用新的状态结构
+ const knowledgeBase = useSelector((state) => state.knowledgeBase.currentKnowledgeBase);
+ const loading = useSelector((state) => state.knowledgeBase.loading);
+ const error = useSelector((state) => state.knowledgeBase.error);
+
+ // Fetch knowledge base details when component mounts or ID changes
+ useEffect(() => {
+ if (id) {
+ dispatch(getKnowledgeBaseById(id));
+ }
+ }, [dispatch, id]);
+
+ // Update active tab when URL changes
+ useEffect(() => {
+ if (tab) {
+ setActiveTab(tab === 'settings' ? 'settings' : 'datasets');
+ }
+ }, [tab]);
+
+ // If knowledge base not found, show notification and redirect
+ useEffect(() => {
+ if (!loading && error) {
+ dispatch(
+ showNotification({
+ message: `获取知识库失败: ${error.message || '未找到知识库'}`,
+ type: 'warning',
+ })
+ );
+ navigate('/knowledge-base');
+ }
+ }, [loading, error, dispatch, navigate]);
+
+ // Handle tab change
+ const handleTabChange = (tab) => {
+ setActiveTab(tab);
+ navigate(`/knowledge-base/${id}/${tab}`);
+ };
+
+ // Show loading state if knowledge base not loaded yet
+ if (loading || !knowledgeBase) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {/* Sidebar */}
+
+
+ {/* Main content */}
+
+ {/* Render the appropriate tab component */}
+ {activeTab === 'datasets' ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx
new file mode 100644
index 0000000..ae5014b
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx
@@ -0,0 +1,348 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { showNotification } from '../../../store/notification.slice';
+import {
+ updateKnowledgeBase,
+ deleteKnowledgeBase,
+ changeKnowledgeBaseType,
+} from '../../../store/knowledgeBase/knowledgeBase.thunks';
+
+// 导入拆分的组件
+import Breadcrumb from './components/Breadcrumb';
+import KnowledgeBaseForm from './components/KnowledgeBaseForm';
+import DeleteConfirmModal from './components/DeleteConfirmModal';
+import UserPermissionsManager from './components/UserPermissionsManager';
+
+// 部门和组别的映射关系
+const departmentGroups = {
+ 技术部: ['开发组', '测试组', '运维组', '架构组', '安全组'],
+ 产品部: ['产品规划组', '用户研究组', '交互设计组', '项目管理组'],
+ 市场部: ['品牌推广组', '市场调研组', '客户关系组', '社交媒体组'],
+ 行政部: ['人事组', '财务组', '行政管理组', '后勤组'],
+};
+
+// 部门列表
+const departments = Object.keys(departmentGroups);
+
+export default function SettingsTab({ knowledgeBase }) {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const currentUser = useSelector((state) => state.auth.user);
+ const isAdmin = currentUser?.role === 'admin';
+
+ // State for knowledge base form
+ const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({
+ name: knowledgeBase.name,
+ desc: knowledgeBase.desc || knowledgeBase.description || '',
+ type: knowledgeBase.type || 'private', // 默认为私有知识库
+ original_type: knowledgeBase.type || 'private', // 存储原始类型用于比较
+ department: knowledgeBase.department || '',
+ group: knowledgeBase.group || '',
+ original_department: knowledgeBase.department || '',
+ original_group: knowledgeBase.group || '',
+ });
+ const [formErrors, setFormErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [availableGroups, setAvailableGroups] = useState([]);
+
+ // 当部门变化时,更新可选的组别
+ useEffect(() => {
+ if (knowledgeBaseForm.department && departmentGroups[knowledgeBaseForm.department]) {
+ setAvailableGroups(departmentGroups[knowledgeBaseForm.department]);
+ // 如果已选择的组别不在新部门的选项中,则重置组别
+ if (!departmentGroups[knowledgeBaseForm.department].includes(knowledgeBaseForm.group)) {
+ setKnowledgeBaseForm((prev) => ({
+ ...prev,
+ group: '',
+ }));
+ }
+ } else {
+ setAvailableGroups([]);
+ setKnowledgeBaseForm((prev) => ({
+ ...prev,
+ group: '',
+ }));
+ }
+ }, [knowledgeBaseForm.department]);
+
+ // 初始化可用组别
+ useEffect(() => {
+ if (knowledgeBase.department && departmentGroups[knowledgeBase.department]) {
+ setAvailableGroups(departmentGroups[knowledgeBase.department]);
+ }
+ }, [knowledgeBase]);
+
+ // Handle knowledge base form input change
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+
+ // 检查如果是类型更改,确保用户有权限
+ if (name === 'type') {
+ const role = currentUser?.role;
+ let allowed = false;
+
+ // 根据角色判断可以选择的知识库类型
+ if (role === 'admin') {
+ // 管理员可以选择任何类型
+ allowed = ['admin', 'leader', 'member', 'private', 'secret'].includes(value);
+ } else if (role === 'leader') {
+ // 组长只能选择 member 和 private
+ allowed = ['member', 'private'].includes(value);
+ } else {
+ // 普通成员只能选择 private
+ allowed = value === 'private';
+ }
+
+ if (!allowed) {
+ dispatch(
+ showNotification({
+ message: '您没有权限设置此类型的知识库',
+ type: 'warning',
+ })
+ );
+ return;
+ }
+ }
+
+ setKnowledgeBaseForm((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ // Clear error if exists
+ if (formErrors[name]) {
+ setFormErrors((prev) => ({
+ ...prev,
+ [name]: '',
+ }));
+ }
+ };
+
+ // Validate knowledge base form
+ const validateForm = () => {
+ const errors = {};
+ // 私有知识库不需要部门和组别
+ const isPrivate = knowledgeBaseForm.type === 'private';
+
+ if (!knowledgeBaseForm.name.trim()) {
+ errors.name = '请输入知识库名称';
+ }
+
+ if (!knowledgeBaseForm.desc.trim()) {
+ errors.desc = '请输入知识库描述';
+ }
+
+ if (!knowledgeBaseForm.type) {
+ errors.type = '请选择知识库类型';
+ }
+
+ // 只有非私有知识库需要验证部门和组别
+ if (!isPrivate) {
+ if (isAdmin && !knowledgeBaseForm.department) {
+ errors.department = '请选择部门';
+ }
+
+ if (isAdmin && !knowledgeBaseForm.group) {
+ errors.group = '请选择组别';
+ }
+ }
+
+ setFormErrors(errors);
+ return Object.keys(errors).length === 0;
+ };
+
+ // 检查是否有部门或组别变更
+ const hasDepartmentOrGroupChanged = () => {
+ return (
+ knowledgeBaseForm.department !== knowledgeBaseForm.original_department ||
+ knowledgeBaseForm.group !== knowledgeBaseForm.original_group
+ );
+ };
+
+ // 单独处理类型更改
+ const handleTypeChange = (newType) => {
+ if (!currentUser) {
+ dispatch(
+ showNotification({
+ message: '用户信息不完整,无法更改类型',
+ type: 'warning',
+ })
+ );
+ return;
+ }
+
+ // 所有用户都可以修改为admin或private类型,无需额外检查
+ if (newType !== 'admin' && newType !== 'private' && currentUser.role === 'member') {
+ dispatch(
+ showNotification({
+ message: '您只能将知识库修改为公共(admin)或私有(private)类型',
+ type: 'warning',
+ })
+ );
+ return;
+ }
+
+ if (isAdmin && !validateForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ // 私有知识库不需要部门和组别
+ const isPrivate = newType === 'private';
+ const department = isPrivate ? '' : isAdmin ? knowledgeBaseForm.department : currentUser.department || '';
+ const group = isPrivate ? '' : isAdmin ? knowledgeBaseForm.group : currentUser.group || '';
+
+ dispatch(
+ changeKnowledgeBaseType({
+ id: knowledgeBase.id,
+ type: newType,
+ department,
+ group,
+ })
+ )
+ .unwrap()
+ .then((updatedKB) => {
+ // 更新表单显示
+ setKnowledgeBaseForm((prev) => ({
+ ...prev,
+ type: updatedKB.type,
+ original_type: updatedKB.type,
+ department: updatedKB.department,
+ original_department: updatedKB.department,
+ group: updatedKB.group,
+ original_group: updatedKB.group,
+ }));
+
+ dispatch(
+ showNotification({
+ message: `知识库类型已更新为 ${updatedKB.type}`,
+ type: 'success',
+ })
+ );
+ setIsSubmitting(false);
+ })
+ .catch((error) => {
+ dispatch(
+ showNotification({
+ message: `类型更新失败: ${error.message || '未知错误'}`,
+ type: 'danger',
+ })
+ );
+ setIsSubmitting(false);
+ });
+ };
+
+ // Handle form submission
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+ // Validate form
+ if (!validateForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ // 检查类型是否有更改,如果有,则使用单独的API更新类型
+ if (knowledgeBaseForm.type !== knowledgeBaseForm.original_type || (isAdmin && hasDepartmentOrGroupChanged())) {
+ handleTypeChange(knowledgeBaseForm.type);
+ return;
+ }
+
+ // Dispatch update knowledge base action (不包含类型更改)
+ dispatch(
+ updateKnowledgeBase({
+ id: knowledgeBase.id,
+ data: {
+ name: knowledgeBaseForm.name,
+ desc: knowledgeBaseForm.desc,
+ description: knowledgeBaseForm.desc, // Add description field for compatibility
+ },
+ })
+ )
+ .unwrap()
+ .then(() => {
+ dispatch(
+ showNotification({
+ message: '知识库更新成功',
+ type: 'success',
+ })
+ );
+ setIsSubmitting(false);
+ })
+ .catch((error) => {
+ dispatch(
+ showNotification({
+ message: `更新失败: ${error.message || '未知错误'}`,
+ type: 'danger',
+ })
+ );
+ setIsSubmitting(false);
+ });
+ };
+
+ // Handle knowledge base deletion
+ const handleDelete = () => {
+ setIsSubmitting(true);
+
+ // Dispatch delete knowledge base action
+ dispatch(deleteKnowledgeBase(knowledgeBase.id))
+ .unwrap()
+ .then(() => {
+ dispatch(
+ showNotification({
+ message: '知识库已删除',
+ type: 'success',
+ })
+ );
+ // Navigate back to knowledge base list
+ navigate('/knowledge-base');
+ })
+ .catch((error) => {
+ dispatch(
+ showNotification({
+ message: `删除失败: ${error.message || '未知错误'}`,
+ type: 'danger',
+ })
+ );
+ setIsSubmitting(false);
+ setShowDeleteConfirm(false);
+ });
+ };
+
+ return (
+ <>
+ {/* Breadcrumb navigation */}
+
+
+ {/* Knowledge Base Form */}
+ setShowDeleteConfirm(true)}
+ onTypeChange={handleTypeChange}
+ isAdmin={isAdmin}
+ departments={departments}
+ availableGroups={availableGroups}
+ />
+
+ {/* User Permissions Manager */}
+ {/* */}
+
+ {/* Delete confirmation modal */}
+ setShowDeleteConfirm(false)}
+ onConfirm={handleDelete}
+ />
+ >
+ );
+}
diff --git a/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx
new file mode 100644
index 0000000..b3bc408
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+/**
+ * 面包屑导航组件
+ */
+const Breadcrumb = ({ knowledgeBase, activeTab }) => {
+ return (
+
+
+
+ );
+};
+
+export default Breadcrumb;
diff --git a/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx
new file mode 100644
index 0000000..e8550f1
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+
+/**
+ * 删除确认模态框组件
+ */
+const DeleteConfirmModal = ({ show, title, isSubmitting, onCancel, onConfirm }) => {
+ if (!show) return null;
+
+ return (
+
+
+
+
确认删除
+
+
+
+
您确定要删除知识库 "{title}" 吗?此操作不可撤销。
+
+
+
+
+
+
+
+ );
+};
+
+export default DeleteConfirmModal;
diff --git a/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx
new file mode 100644
index 0000000..747832a
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import SvgIcon from '../../../../components/SvgIcon';
+
+/**
+ * 文档列表组件
+ */
+const DocumentList = ({ documents, selectedDocuments, onSelectAll, onSelectDocument, onDeleteDocument, selectAll }) => {
+ if (documents.length === 0) {
+ return 暂无数据集,请上传数据集
;
+ }
+
+ return (
+
+ );
+};
+
+export default DocumentList;
diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
new file mode 100644
index 0000000..915f85e
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx
@@ -0,0 +1,157 @@
+import React, { useRef, useEffect } from 'react';
+
+/**
+ * 文件上传模态框组件
+ */
+const FileUploadModal = ({
+ show,
+ newFile,
+ fileErrors,
+ isSubmitting,
+ onClose,
+ onDescriptionChange,
+ onFileChange,
+ onFileDrop,
+ onDragOver,
+ onUploadAreaClick,
+ onUpload,
+}) => {
+ const fileInputRef = useRef(null);
+ const modalRef = useRef(null);
+
+ // 处理上传区域点击事件
+ const handleUploadAreaClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ // 处理拖拽事件
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onDragOver?.(e);
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onFileDrop?.(e);
+ };
+
+ // 清理函数
+ useEffect(() => {
+ return () => {
+ // 确保在组件卸载时清理所有引用
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ if (modalRef.current) {
+ modalRef.current = null;
+ }
+ };
+ }, []);
+
+ if (!show) return null;
+
+ return (
+
+
+
+
上传文件
+
+
+
+
+
+ {newFile.file ? (
+
+
已选择文件:
+
{newFile.file.name}
+
+ ) : (
+
+
点击或拖拽文件到此处上传
+
支持 PDF, DOCX, TXT, CSV 等格式
+
+ )}
+ {fileErrors.file &&
{fileErrors.file}
}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FileUploadModal;
diff --git a/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx
new file mode 100644
index 0000000..f53e54f
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx
@@ -0,0 +1,279 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+
+/**
+ * 知识库表单组件
+ */
+const KnowledgeBaseForm = ({
+ formData,
+ formErrors,
+ isSubmitting,
+ onInputChange,
+ onSubmit,
+ onDelete,
+ onTypeChange,
+ isAdmin,
+ departments,
+ availableGroups,
+}) => {
+ // 获取当前用户信息
+ const currentUser = useSelector((state) => state.auth.user);
+
+ // 根据用户角色确定可以设置的知识库类型
+ const isLeader = currentUser?.role === 'leader';
+
+ // 获取当前用户可以设置的知识库类型
+ const getAvailableTypes = () => {
+ if (isAdmin) {
+ return [
+ { value: 'admin', label: '公共知识库' },
+ { value: 'leader', label: 'Leader 级知识库' },
+ { value: 'member', label: 'Member 级知识库' },
+ { value: 'private', label: '私有知识库' },
+ { value: 'secret', label: '保密知识库' },
+ ];
+ } else if (isLeader) {
+ return [
+ { value: 'admin', label: '公共知识库' },
+ { value: 'member', label: 'Member 级知识库' },
+ { value: 'private', label: '私有知识库' },
+ ];
+ } else {
+ return [
+ { value: 'admin', label: '公共知识库' },
+ { 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 (
+
+ );
+};
+
+export default KnowledgeBaseForm;
diff --git a/src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx b/src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx
new file mode 100644
index 0000000..afcc39b
--- /dev/null
+++ b/src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx
@@ -0,0 +1,789 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { useDispatch } from 'react-redux';
+import { showNotification } from '../../../../store/notification.slice';
+import SvgIcon from '../../../../components/SvgIcon';
+
+/**
+ * 用户权限管理组件
+ */
+const UserPermissionsManager = ({ knowledgeBase }) => {
+ const dispatch = useDispatch();
+
+ // State for pagination
+ const [currentPage, setCurrentPage] = useState(1);
+ const usersPerPage = 10;
+
+ // State for edit modal
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [editUser, setEditUser] = useState(null);
+
+ // State for add user modal
+ const [showAddUserModal, setShowAddUserModal] = useState(false);
+ const [newUser, setNewUser] = useState({
+ username: '',
+ email: '',
+ permissionType: '只读',
+ accessDuration: '一个月',
+ });
+
+ // State for batch operations
+ const [selectedUsers, setSelectedUsers] = useState([]);
+ const [selectAll, setSelectAll] = useState(false);
+ const [showBatchDropdown, setShowBatchDropdown] = useState(false);
+ const batchDropdownRef = useRef(null);
+
+ // State for batch edit modal
+ const [showBatchEditModal, setShowBatchEditModal] = useState(false);
+ const [batchEditData, setBatchEditData] = useState({
+ permissionType: '只读',
+ accessDuration: '一个月',
+ });
+
+ // Form errors
+ const [formErrors, setFormErrors] = useState({});
+
+ // Mock data for users with permissions
+ const [users, setUsers] = useState([
+ {
+ id: '1001',
+ username: '张三',
+ email: 'zhang@abc.com',
+ permissionType: '只读',
+ accessDuration: '一个月',
+ },
+ {
+ id: '1002',
+ username: '李四',
+ email: 'li@abc.com',
+ permissionType: '完全访问',
+ accessDuration: '永久',
+ },
+ {
+ id: '1003',
+ username: '王五',
+ email: 'wang@abc.com',
+ permissionType: '只读',
+ accessDuration: '三个月',
+ },
+ {
+ id: '1004',
+ username: '赵六',
+ email: 'zhao@abc.com',
+ permissionType: '完全访问',
+ accessDuration: '六个月',
+ },
+ {
+ id: '1005',
+ username: '钱七',
+ email: 'qian@abc.com',
+ permissionType: '只读',
+ accessDuration: '一周',
+ },
+ {
+ id: '1006',
+ username: '孙八',
+ email: 'sun@abc.com',
+ permissionType: '只读',
+ accessDuration: '一个月',
+ },
+ {
+ id: '1007',
+ username: '周九',
+ email: 'zhou@abc.com',
+ permissionType: '完全访问',
+ accessDuration: '永久',
+ },
+ {
+ id: '1008',
+ username: '吴十',
+ email: 'wu@abc.com',
+ permissionType: '只读',
+ accessDuration: '三个月',
+ },
+ ]);
+
+ // Get current users for pagination
+ const indexOfLastUser = currentPage * usersPerPage;
+ const indexOfFirstUser = indexOfLastUser - usersPerPage;
+ const currentUsers = users.slice(indexOfFirstUser, indexOfLastUser);
+ const totalPages = Math.ceil(users.length / usersPerPage);
+
+ // Handle click outside batch dropdown
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (batchDropdownRef.current && !batchDropdownRef.current.contains(event.target)) {
+ setShowBatchDropdown(false);
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ // Handle edit user permissions
+ const handleEditUser = (user) => {
+ setEditUser({ ...user });
+ setFormErrors({});
+ setShowEditModal(true);
+ };
+
+ // Handle input change in edit modal
+ const handleEditInputChange = (e) => {
+ const { name, value } = e.target;
+ setEditUser((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ // Clear error if exists
+ if (formErrors[name]) {
+ setFormErrors((prev) => ({
+ ...prev,
+ [name]: '',
+ }));
+ }
+ };
+
+ // Handle input change in add user modal
+ const handleAddUserInputChange = (e) => {
+ const { name, value } = e.target;
+ setNewUser((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ // Clear error if exists
+ if (formErrors[name]) {
+ setFormErrors((prev) => ({
+ ...prev,
+ [name]: '',
+ }));
+ }
+ };
+
+ // Handle input change in batch edit modal
+ const handleBatchEditInputChange = (e) => {
+ const { name, value } = e.target;
+ setBatchEditData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ // Validate edit form
+ const validateEditForm = () => {
+ const errors = {};
+
+ if (!editUser.username.trim()) {
+ errors.username = '请输入用户名';
+ }
+
+ if (!editUser.email.trim()) {
+ errors.email = '请输入邮箱';
+ } else if (!/\S+@\S+\.\S+/.test(editUser.email)) {
+ errors.email = '请输入有效的邮箱地址';
+ }
+
+ setFormErrors(errors);
+ return Object.keys(errors).length === 0;
+ };
+
+ // Validate add user form
+ const validateAddUserForm = () => {
+ const errors = {};
+
+ if (!newUser.username.trim()) {
+ errors.username = '请输入用户名';
+ }
+
+ if (!newUser.email.trim()) {
+ errors.email = '请输入邮箱';
+ } else if (!/\S+@\S+\.\S+/.test(newUser.email)) {
+ errors.email = '请输入有效的邮箱地址';
+ }
+
+ setFormErrors(errors);
+ return Object.keys(errors).length === 0;
+ };
+
+ // Handle save user permissions
+ const handleSaveUserPermissions = () => {
+ // Validate form
+ if (!validateEditForm()) {
+ return;
+ }
+
+ // Here you would typically call an API to update the user permissions
+ console.log('Updating user permissions:', editUser);
+
+ // Update the users array with the edited user
+ setUsers((prevUsers) => prevUsers.map((user) => (user.id === editUser.id ? { ...editUser } : user)));
+
+ // Show success notification
+ dispatch(showNotification({ message: '用户权限已更新', type: 'success' }));
+
+ // Close modal
+ setShowEditModal(false);
+ };
+
+ // Handle add new user
+ const handleAddUser = () => {
+ // Validate form
+ if (!validateAddUserForm()) {
+ return;
+ }
+
+ // Generate a new ID
+ const newId = `user-${Date.now()}`;
+
+ // Create new user object
+ const userToAdd = {
+ id: newId,
+ username: newUser.username,
+ email: newUser.email,
+ permissionType: newUser.permissionType,
+ accessDuration: newUser.accessDuration,
+ };
+
+ // Add new user to the users array
+ setUsers((prevUsers) => [...prevUsers, userToAdd]);
+
+ // Show success notification
+ dispatch(showNotification({ message: '用户已添加', type: 'success' }));
+
+ // Reset form and close modal
+ setNewUser({
+ username: '',
+ email: '',
+ permissionType: '只读',
+ accessDuration: '一个月',
+ });
+ setShowAddUserModal(false);
+ };
+
+ // Handle delete user
+ const handleDeleteUser = (userId) => {
+ // Here you would typically call an API to delete the user
+ console.log('Deleting user:', userId);
+
+ // Remove user from the users array
+ setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId));
+
+ // Remove from selected users if present
+ setSelectedUsers((prev) => prev.filter((id) => id !== userId));
+
+ // Show success notification
+ dispatch(showNotification({ message: '用户已删除', type: 'success' }));
+ };
+
+ // Handle batch delete
+ const handleBatchDelete = () => {
+ if (selectedUsers.length === 0) return;
+
+ // Here you would typically call an API to delete the selected users
+ console.log('Batch deleting users:', selectedUsers);
+
+ // Remove selected users from the users array
+ setUsers((prevUsers) => prevUsers.filter((user) => !selectedUsers.includes(user.id)));
+
+ // Reset selection
+ setSelectedUsers([]);
+ setSelectAll(false);
+ setShowBatchDropdown(false);
+
+ // Show success notification
+ dispatch(showNotification({ message: `已删除 ${selectedUsers.length} 个用户`, type: 'success' }));
+ };
+
+ // Handle batch edit
+ const handleBatchEdit = () => {
+ if (selectedUsers.length === 0) return;
+
+ // Here you would typically call an API to update the selected users
+ console.log('Batch editing users:', selectedUsers, 'with data:', batchEditData);
+
+ // Update selected users in the users array
+ setUsers((prevUsers) =>
+ prevUsers.map((user) => (selectedUsers.includes(user.id) ? { ...user, ...batchEditData } : user))
+ );
+
+ // Close modal
+ setShowBatchEditModal(false);
+ setShowBatchDropdown(false);
+
+ // Show success notification
+ dispatch(showNotification({ message: `已更新 ${selectedUsers.length} 个用户的权限`, type: 'success' }));
+ };
+
+ // Handle select all checkbox
+ const handleSelectAll = () => {
+ if (selectAll) {
+ setSelectedUsers([]);
+ } else {
+ setSelectedUsers(currentUsers.map((user) => user.id));
+ }
+ setSelectAll(!selectAll);
+ };
+
+ // Handle individual user selection
+ const handleSelectUser = (userId) => {
+ if (selectedUsers.includes(userId)) {
+ setSelectedUsers(selectedUsers.filter((id) => id !== userId));
+ setSelectAll(false);
+ } else {
+ setSelectedUsers([...selectedUsers, userId]);
+ // Check if all current page users are now selected
+ if (selectedUsers.length + 1 === currentUsers.length) {
+ setSelectAll(true);
+ }
+ }
+ };
+
+ // Handle page change
+ const handlePageChange = (pageNumber) => {
+ setCurrentPage(pageNumber);
+ // Reset selection when changing pages
+ setSelectedUsers([]);
+ setSelectAll(false);
+ };
+
+ return (
+
+
+
+
用户权限管理
+
+ {selectedUsers.length > 0 && (
+
+
+ {showBatchDropdown && (
+
+ -
+
+
+ -
+
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ )}
+
+ {/* Edit User Modal */}
+ {showEditModal && (
+
+
+
+
+
编辑用户权限
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Add User Modal */}
+ {showAddUserModal && (
+
+
+
+
+
添加用户
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Batch Edit Modal */}
+ {showBatchEditModal && (
+
+
+
+
+
批量修改权限
+
+
+
+
+ 您正在修改 {selectedUsers.length} 个用户的权限
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Modal backdrop */}
+ {(showEditModal || showAddUserModal || showBatchEditModal) && (
+
+ )}
+
+
+ );
+};
+
+export default UserPermissionsManager;
diff --git a/src/pages/KnowledgeBase/KnowledgeBase.css b/src/pages/KnowledgeBase/KnowledgeBase.css
new file mode 100644
index 0000000..785612f
--- /dev/null
+++ b/src/pages/KnowledgeBase/KnowledgeBase.css
@@ -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%;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx
index 2db1539..4240de1 100644
--- a/src/pages/KnowledgeBase/KnowledgeBase.jsx
+++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx
@@ -1,48 +1,535 @@
-import React from 'react';
-import KnowledgeCard from './KnowledgeCard';
-import { useDispatch } from 'react-redux';
+import React, { useState, useEffect, useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { showNotification } from '../../store/notification.slice';
+import {
+ fetchKnowledgeBases,
+ searchKnowledgeBases,
+ createKnowledgeBase,
+ deleteKnowledgeBase,
+ requestKnowledgeBaseAccess,
+} from '../../store/knowledgeBase/knowledgeBase.thunks';
+import { clearSearchResults } from '../../store/knowledgeBase/knowledgeBase.slice';
+import SvgIcon from '../../components/SvgIcon';
+import AccessRequestModal from '../../components/AccessRequestModal';
+import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
+import Pagination from '../../components/Pagination';
+import SearchBar from '../../components/SearchBar';
+import ApiModeSwitch from '../../components/ApiModeSwitch';
+
+// 导入拆分的组件
+import KnowledgeBaseList from './components/KnowledgeBaseList';
export default function KnowledgeBase() {
const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showAccessRequestModal, setShowAccessRequestModal] = useState(false);
+ const [formErrors, setFormErrors] = useState({});
+ const [accessRequestKnowledgeBase, setAccessRequestKnowledgeBase] = useState({
+ id: '',
+ title: '',
+ });
+ const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
+ const [createdKnowledgeBaseId, setCreatedKnowledgeBaseId] = useState(null);
- const knowledgeList = [
- {
- title: '产品开发知识库',
- description: '产品开发流程及规范说明文档',
- documents: 24,
- date: '2025-02-15',
- access: 'full',
- },
- {
- title: '市场分析知识库',
- description: '2025年Q1市场分析总结',
- documents: 12,
- date: '2025-02-10',
- access: 'read',
- },
- { title: '财务知识库', description: '月度财务分析报告', documents: 8, date: '2025-02-01', access: 'none' },
- ];
+ // 获取当前用户信息
+ const currentUser = useSelector((state) => state.auth.user);
+
+ const [newKnowledgeBase, setNewKnowledgeBase] = useState({
+ name: '',
+ desc: '',
+ type: 'private', // 默认为私有知识库
+ department: currentUser?.department || '',
+ group: currentUser?.group || '',
+ });
+
+ // Search state
+ const [searchKeyword, setSearchKeyword] = useState('');
+ const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
+
+ // Pagination state
+ const [pagination, setPagination] = useState({
+ page: 1,
+ page_size: 10,
+ });
+
+ // Get knowledge bases from Redux store
+ // 更新为新的Redux状态结构
+ const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases);
+ const loading = useSelector((state) => state.knowledgeBase.loading);
+ const paginationData = useSelector((state) => state.knowledgeBase.pagination);
+ const error = useSelector((state) => state.knowledgeBase.error);
+ const operationStatus = useSelector((state) => state.knowledgeBase.editStatus);
+ const operationError = useSelector((state) => state.knowledgeBase.error);
+
+ // 从Redux获取搜索结果和加载状态
+ const searchResults = useSelector((state) => state.knowledgeBase.searchResults);
+ const searchLoading = useSelector((state) => state.knowledgeBase.searchLoading);
+
+ // Fetch knowledge bases when component mounts or pagination changes
+ useEffect(() => {
+ // 无论是否在搜索,都正常获取知识库列表
+ dispatch(fetchKnowledgeBases(pagination));
+ }, [dispatch, pagination.page, pagination.page_size]);
+
+ // Show loading state while fetching data
+ const isLoading = loading;
+
+ // Show error notification if fetch fails
+ useEffect(() => {
+ if (!isLoading && error) {
+ dispatch(
+ showNotification({
+ message: `获取知识库列表失败: ${error.message || error}`,
+ type: 'danger',
+ })
+ );
+ }
+ }, [isLoading, error, dispatch]);
+
+ // Show notification for operation status
+ useEffect(() => {
+ if (operationStatus === 'successful') {
+ // 操作成功通知由具体函数处理,这里只刷新列表
+
+ // Refresh the list after successful operation
+ dispatch(fetchKnowledgeBases(pagination));
+ } else if (operationStatus === 'failed' && operationError) {
+ dispatch(
+ showNotification({
+ message: `操作失败: ${operationError.message || operationError}`,
+ type: 'danger',
+ })
+ );
+ }
+ }, [operationStatus, operationError, dispatch, pagination]);
+
+ // Handle search input change
+ const handleSearchInputChange = (e) => {
+ const value = e.target.value;
+ setSearchKeyword(value);
+
+ // 如果搜索框清空,关闭下拉框
+ if (!value.trim()) {
+ dispatch(clearSearchResults());
+ setIsSearchDropdownOpen(false);
+ }
+ };
+
+ // Handle search submit - 只影响下拉框,不影响主列表
+ const handleSearch = (e) => {
+ e.preventDefault();
+
+ if (searchKeyword.trim()) {
+ // 只设置下拉框搜索状态,不设置全局isSearching状态
+ dispatch(
+ searchKnowledgeBases({
+ keyword: searchKeyword,
+ page: 1,
+ page_size: 5, // 下拉框只显示少量结果
+ })
+ );
+ setIsSearchDropdownOpen(true);
+ } else {
+ // 清空搜索及关闭下拉框
+ handleClearSearch();
+ }
+ };
+
+ // Handle clear search
+ const handleClearSearch = () => {
+ setSearchKeyword('');
+ // 不影响主列表显示,只关闭下拉框
+ setIsSearchDropdownOpen(false);
+ dispatch(clearSearchResults());
+ };
+
+ // Handle pagination change
+ const handlePageChange = (newPage) => {
+ setPagination((prev) => ({
+ ...prev,
+ page: newPage,
+ }));
+ };
+
+ // Handle page size change
+ const handlePageSizeChange = (newPageSize) => {
+ setPagination({
+ page: 1, // Reset to first page when changing page size
+ page_size: newPageSize,
+ });
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ const isAdmin = currentUser?.role === 'admin';
+ const isLeader = currentUser?.role === 'leader';
+
+ // 根据用户角色允许修改部门和组别字段
+ if (name === 'department' || name === 'group') {
+ // 仅管理员可以修改部门
+ if (name === 'department' && !isAdmin) {
+ return;
+ }
+
+ // 仅管理员和组长可以修改组别
+ if (name === 'group' && !isAdmin && !isLeader) {
+ return;
+ }
+
+ // 更新字段值
+ setNewKnowledgeBase((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ // 如果更新部门,重置组别
+ if (name === 'department') {
+ setNewKnowledgeBase((prev) => ({
+ ...prev,
+ group: '', // 部门变更时重置组别
+ }));
+ }
+
+ return;
+ }
+
+ // 检查用户是否有权限选择指定的知识库类型
+ if (name === 'type') {
+ const role = currentUser?.role;
+ let allowed = false;
+
+ // 根据角色判断可以选择的知识库类型
+ if (role === 'admin') {
+ // 管理员可以选择任何类型
+ allowed = ['admin', 'leader', 'member', 'private', 'secret'].includes(value);
+ } else if (role === 'leader') {
+ // 组长只能选择 member 和 private
+ allowed = ['member', 'private'].includes(value);
+ } else {
+ // 普通成员只能选择 private
+ allowed = value === 'private';
+ }
+
+ if (!allowed) {
+ dispatch(
+ showNotification({
+ message: '您没有权限创建此类型的知识库',
+ type: 'warning',
+ })
+ );
+ return;
+ }
+ }
+
+ setNewKnowledgeBase((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ // Clear error when user types
+ if (formErrors[name]) {
+ setFormErrors((prev) => ({
+ ...prev,
+ [name]: '',
+ }));
+ }
+ };
+
+ const validateCreateForm = () => {
+ const errors = {};
+ const isAdmin = currentUser?.role === 'admin';
+ const isLeader = currentUser?.role === 'leader';
+ // 只有member类型知识库需要选择组别,私有知识库不需要
+ const needSelectGroup = newKnowledgeBase.type === 'member';
+ // 私有知识库不需要选择部门和组别
+ const isPrivate = newKnowledgeBase.type === 'private';
+
+ if (!newKnowledgeBase.name.trim()) {
+ errors.name = '请输入知识库名称';
+ }
+
+ if (!newKnowledgeBase.desc.trim()) {
+ errors.desc = '请输入知识库描述';
+ }
+
+ if (!newKnowledgeBase.type) {
+ errors.type = '请选择知识库类型';
+ }
+
+ // 对于member级别的知识库,检查是否选择了部门和组别
+ if (needSelectGroup && !isPrivate) {
+ // 管理员必须选择部门
+ if (isAdmin && !newKnowledgeBase.department) {
+ errors.department = '创建member级别知识库时必须选择部门';
+ }
+
+ // 所有用户创建member级别知识库时必须选择组别
+ if (!newKnowledgeBase.group) {
+ errors.group = '创建member级别知识库时必须选择组别';
+ }
+ }
+
+ setFormErrors(errors);
+ return Object.keys(errors).length === 0;
+ };
+
+ const handleCreateKnowledgeBase = async () => {
+ // Validate form
+ if (!validateCreateForm()) {
+ return;
+ }
+
+ try {
+ // 私有知识库不需要部门和组别信息
+ const isPrivate = newKnowledgeBase.type === 'private';
+
+ // Dispatch create knowledge base action
+ const resultAction = await dispatch(
+ createKnowledgeBase({
+ name: newKnowledgeBase.name,
+ desc: newKnowledgeBase.desc,
+ description: newKnowledgeBase.desc,
+ type: newKnowledgeBase.type,
+ department: !isPrivate ? newKnowledgeBase.department : '',
+ group: !isPrivate ? newKnowledgeBase.group : '',
+ })
+ );
+
+ console.log('创建知识库返回数据:', resultAction);
+
+ // Check if the action was successful
+ if (createKnowledgeBase.fulfilled.match(resultAction)) {
+ console.log('创建成功,payload:', resultAction.payload);
+ const { knowledge_base } = resultAction.payload;
+ const { id } = knowledge_base;
+ // Get ID from payload and navigate
+ if (id) {
+ console.log('新知识库ID:', id);
+
+ // 显示成功通知
+ dispatch(
+ showNotification({
+ message: '知识库创建成功',
+ type: 'success',
+ })
+ );
+
+ // 直接导航到新创建的知识库详情页
+ navigate(`/knowledge-base/${id}`);
+ } else {
+ console.error('无法获取新知识库ID:', resultAction.payload);
+ dispatch(
+ showNotification({
+ message: '创建成功,但无法获取知识库ID',
+ type: 'warning',
+ })
+ );
+ }
+ } else {
+ console.error('创建知识库失败:', resultAction.error);
+ dispatch(
+ showNotification({
+ message: `创建知识库失败: ${resultAction.error?.message || '未知错误'}`,
+ type: 'danger',
+ })
+ );
+ }
+ } catch (error) {
+ console.error('创建知识库出错:', error);
+ dispatch(
+ showNotification({
+ message: `创建知识库出错: ${error.message || '未知错误'}`,
+ type: 'danger',
+ })
+ );
+ }
+
+ // Reset form and close modal
+ setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
+ setFormErrors({});
+ setShowCreateModal(false);
+ };
+
+ // Handle card click to navigate to knowledge base detail
+ const handleCardClick = (id, permissions) => {
+ // 检查用户是否有读取权限
+ if (!permissions || permissions.can_read === false) {
+ dispatch(
+ showNotification({
+ message: '您没有访问此知识库的权限,请先申请权限',
+ type: 'warning',
+ })
+ );
+ return;
+ }
+
+ // 有权限则跳转到详情页
+ navigate(`/knowledge-base/${id}/datasets`);
+ };
+
+ const handleRequestAccess = (id, title) => {
+ setAccessRequestKnowledgeBase({
+ id,
+ title,
+ });
+ setShowAccessRequestModal(true);
+ };
+
+ const handleSubmitAccessRequest = async (requestData) => {
+ setIsSubmittingRequest(true);
+
+ try {
+ // 使用权限服务发送请求 - 通过dispatch调用thunk
+ await dispatch(requestKnowledgeBaseAccess(requestData)).unwrap();
+
+ // Close modal after success
+ setShowAccessRequestModal(false);
+ } catch (error) {
+ dispatch(
+ showNotification({
+ message: `权限申请失败: ${error.response?.data?.message || '请稍后重试'}`,
+ type: 'danger',
+ })
+ );
+ } finally {
+ setIsSubmittingRequest(false);
+ }
+ };
+
+ const handleDelete = (e, id) => {
+ e.preventDefault();
+ e.stopPropagation();
+ // 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
+ const totalPages = Math.ceil(paginationData.total / pagination.page_size);
+
+ // 打开创建知识库弹窗
+ const handleOpenCreateModal = () => {
+ const isAdmin = currentUser?.role === 'admin';
+ const isLeader = currentUser?.role === 'leader';
+
+ // 默认知识库类型设为公共知识库
+ let defaultType = 'admin';
+
+ // 初始部门和组别
+ let department = currentUser?.department || '';
+ let group = currentUser?.group || '';
+
+ setNewKnowledgeBase({
+ name: '',
+ desc: '',
+ type: defaultType,
+ department: department,
+ group: group,
+ });
+
+ setFormErrors({});
+ setShowCreateModal(true);
+ };
+
+ // 处理点击搜索结果
+ const handleSearchResultClick = (id, permissions) => {
+ if (permissions?.can_read) {
+ navigate(`/knowledge-base/${id}/datasets`);
+ }
+ };
return (
-
+
+ {/*
*/}
-
- {knowledgeList.map((item, index) => (
-
- ))}
-
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+ {/* Pagination - 始终显示 */}
+ {totalPages > 1 && (
+
+ )}
+ >
+ )}
+
+ {/* 新建知识库弹窗 */}
+
setShowCreateModal(false)}
+ onChange={handleInputChange}
+ onSubmit={handleCreateKnowledgeBase}
+ currentUser={currentUser}
+ />
+
+ {/* 申请权限弹窗 */}
+ setShowAccessRequestModal(false)}
+ onSubmit={handleSubmitAccessRequest}
+ isSubmitting={isSubmittingRequest}
+ />
);
}
diff --git a/src/pages/KnowledgeBase/KnowledgeCard.jsx b/src/pages/KnowledgeBase/KnowledgeCard.jsx
deleted file mode 100644
index b14cd6a..0000000
--- a/src/pages/KnowledgeBase/KnowledgeCard.jsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React from 'react';
-
-export default function KnowledgeCard({ title, description, documents, date, access }) {
- return (
-
-
-
-
{title}
-
-
{description}
-
-
- {documents} 文档
-
-
- {date}
-
-
-
- {access === 'full' ? (
-
-
- 完全访问
-
- ) : access === 'read' ? (
-
-
- 只读访问
-
- ) : (
-
-
- 无访问权限
-
- )}
- {access === 'full' || access === 'read' ? (
-
- ) : (
-
- )}
-
-
-
-
- );
-}
diff --git a/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx
new file mode 100644
index 0000000..4b51caf
--- /dev/null
+++ b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import KnowledgeCard from './KnowledgeCard';
+
+/**
+ * 知识库列表组件
+ */
+const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequestAccess, onDelete }) => {
+ if (!knowledgeBases?.length) {
+ return (
+
+ {isSearching ? '没有找到匹配的知识库' : '暂无知识库,请创建新的知识库'}
+
+ );
+ }
+
+ return (
+
+ {knowledgeBases.map((item) => (
+
+ onCardClick(item.id, item.permissions)}
+ onRequestAccess={onRequestAccess}
+ onDelete={(e) => onDelete(e, item.id)}
+ type={item.type}
+ department={item.department}
+ group={item.group}
+ />
+
+ ))}
+
+ );
+};
+
+export default KnowledgeBaseList;
diff --git a/src/pages/KnowledgeBase/components/KnowledgeCard.jsx b/src/pages/KnowledgeBase/components/KnowledgeCard.jsx
new file mode 100644
index 0000000..022f478
--- /dev/null
+++ b/src/pages/KnowledgeBase/components/KnowledgeCard.jsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import SvgIcon from '../../../components/SvgIcon';
+import { useNavigate } from 'react-router-dom';
+
+export default function KnowledgeCard({
+ id,
+ title,
+ description,
+ documents,
+ date,
+ access,
+ permissions,
+ onClick,
+ onRequestAccess,
+ onDelete,
+ type,
+ department,
+ group,
+}) {
+ const navigate = useNavigate();
+
+ const handleNewChat = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ navigate(`/chat/${id}`);
+ };
+
+ const handleRequestAccess = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onRequestAccess(id, title);
+ };
+
+ // 自定义样式,限制文本最多显示两行
+ const descriptionStyle = {
+ display: '-webkit-box',
+ WebkitLineClamp: 2,
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ minHeight: '3rem', // 保持一致的高度,即使描述很短
+ };
+
+ return (
+
+
+
+ {permissions && permissions.can_delete && (
+
+ )}
+
+ {description}
+
+
+
+ {documents} 文档
+
+
+ {date}
+
+
+ {/*
+
+ {type === 'private' ? '私有' : '公开'}
+
+ {department && {department}}
+ {group && {group}}
+
*/}
+
+ {access === 'full' ? (
+
+
+ 完全访问
+
+ ) : access === 'read' ? (
+
+
+ 只读访问
+
+ ) : (
+
+
+ 无访问权限
+
+ )}
+ {access === 'full' || access === 'read' ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/Permissions/Permissions.css b/src/pages/Permissions/Permissions.css
new file mode 100644
index 0000000..5fa7f2d
--- /dev/null
+++ b/src/pages/Permissions/Permissions.css
@@ -0,0 +1,39 @@
+.permissions-container {
+ padding: 24px;
+ background-color: #f8f9fa;
+ min-height: calc(100vh - 64px);
+}
+
+.permissions-section {
+ background-color: #fff;
+ border-radius: 8px;
+ padding: 24px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.api-mode-control {
+ background-color: #fff;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+}
+
+.api-mode-control .api-mode-switch {
+ display: flex;
+ flex-direction: column;
+}
+
+@media (max-width: 768px) {
+ .permissions-container {
+ padding: 16px;
+ }
+
+ .permissions-section,
+ .api-mode-control {
+ padding: 16px;
+ }
+}
+
+.permissions-section:last-child {
+ margin-bottom: 0;
+}
\ No newline at end of file
diff --git a/src/pages/Permissions/PermissionsPage.jsx b/src/pages/Permissions/PermissionsPage.jsx
new file mode 100644
index 0000000..afb8466
--- /dev/null
+++ b/src/pages/Permissions/PermissionsPage.jsx
@@ -0,0 +1,31 @@
+import React, { useEffect } from 'react';
+import { useSelector } from 'react-redux';
+import { useNavigate } from 'react-router-dom';
+import PendingRequests from './components/PendingRequests';
+import UserPermissions from './components/UserPermissions';
+import './Permissions.css';
+
+export default function PermissionsPage() {
+ const navigate = useNavigate();
+ const { user } = useSelector((state) => state.auth);
+
+ // 检查用户是否有管理权限(leader 或 admin)
+ useEffect(() => {
+ if (!user || (user.role !== 'leader' && user.role !== 'admin')) {
+ navigate('/');
+ }
+ }, [user, navigate]);
+
+ return (
+
+
+ {user && user.role === 'admin' && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/Permissions/components/PendingRequests.css b/src/pages/Permissions/components/PendingRequests.css
new file mode 100644
index 0000000..9ddbd81
--- /dev/null
+++ b/src/pages/Permissions/components/PendingRequests.css
@@ -0,0 +1,251 @@
+.permission-requests {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.permission-card {
+ transition: all 0.2s ease;
+ border-radius: 8px;
+ background-color: #343a40;
+}
+
+.permission-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+}
+
+.custom-badge {
+ padding: 0.35em 0.65em;
+ font-size: 0.85em;
+}
+
+.badge.bg-info {
+ background-color: #0dcaf0 !important;
+}
+
+.badge.bg-success {
+ background-color: #198754 !important;
+}
+
+.badge.bg-danger {
+ background-color: #dc3545 !important;
+}
+
+.badge.bg-secondary {
+ background-color: #6c757d !important;
+}
+
+/* 表格行鼠标样式 */
+.cursor-pointer {
+ cursor: pointer;
+}
+
+/* 滑动面板样式 */
+.slide-over-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1040;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease, visibility 0.3s ease;
+}
+
+.slide-over-backdrop.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.slide-over {
+ position: fixed;
+ top: 0;
+ right: -450px;
+ width: 450px;
+ height: 100%;
+ background-color: #fff;
+ z-index: 1050;
+ transition: right 0.3s ease;
+ box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
+}
+
+.slide-over.show {
+ right: 0;
+}
+
+.slide-over-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.slide-over-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ border-bottom: 1px solid #e9ecef;
+}
+
+.slide-over-body {
+ flex: 1;
+ padding: 1rem;
+ overflow-y: auto;
+}
+
+.slide-over-footer {
+ padding: 1rem;
+ border-top: 1px solid #e9ecef;
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+}
+
+/* 头像占位符 */
+.avatar-placeholder {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: #6c757d;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ font-size: 1.2rem;
+}
+
+/* 新增样式 - 白色基调 */
+.badge-count {
+ background-color: #ff4d4f;
+ color: white;
+ border-radius: 20px;
+ padding: 4px 12px;
+ font-size: 14px;
+}
+
+.pending-requests-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.pending-request-item {
+ position: relative;
+ background-color: #fff;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.pending-request-item:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.request-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.user-info h6 {
+ font-weight: 600;
+}
+
+.department {
+ color: #666;
+ font-size: 14px;
+ margin: 0;
+}
+
+.request-date {
+ color: #999;
+ font-size: 14px;
+}
+
+.request-content {
+ color: #333;
+}
+
+.permission-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.permission-badge {
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.permission-badge.read {
+ background-color: #e6f7ff;
+ color: #1890ff;
+ border: 1px solid #91d5ff;
+}
+
+.permission-badge.edit {
+ background-color: #f6ffed;
+ color: #52c41a;
+ border: 1px solid #b7eb8f;
+}
+
+.permission-badge.delete {
+ background-color: #fff2f0;
+ color: #ff4d4f;
+ border: 1px solid #ffccc7;
+}
+
+.request-actions {
+ position: absolute;
+ right: 1rem;
+ bottom: 1rem;
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+
+/* 分页控件样式 */
+.pagination-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 20px;
+ padding: 10px 0;
+}
+
+.pagination-button {
+ background-color: #fff;
+ border: 1px solid #d9d9d9;
+ color: #333;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.2s;
+}
+
+.pagination-button:hover:not(:disabled) {
+ border-color: #1890ff;
+ color: #1890ff;
+}
+
+.pagination-button:disabled {
+ color: #d9d9d9;
+ cursor: not-allowed;
+}
+
+.pagination-info {
+ margin: 0 15px;
+ color: #666;
+ font-size: 14px;
+}
\ No newline at end of file
diff --git a/src/pages/Permissions/components/PendingRequests.jsx b/src/pages/Permissions/components/PendingRequests.jsx
new file mode 100644
index 0000000..09b47e4
--- /dev/null
+++ b/src/pages/Permissions/components/PendingRequests.jsx
@@ -0,0 +1,442 @@
+import React, { useState, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useLocation } from 'react-router-dom';
+import { showNotification } from '../../../store/notification.slice';
+import {
+ fetchPermissionsThunk,
+ approvePermissionThunk,
+ rejectPermissionThunk,
+} from '../../../store/permissions/permissions.thunks';
+import { resetOperationStatus } from '../../../store/permissions/permissions.slice';
+import './PendingRequests.css'; // 引入外部CSS文件
+import SvgIcon from '../../../components/SvgIcon';
+import RequestDetailSlideOver from './RequestDetailSlideOver';
+
+export default function PendingRequests() {
+ const dispatch = useDispatch();
+ const location = useLocation();
+ const [responseMessage, setResponseMessage] = useState('');
+ const [showResponseInput, setShowResponseInput] = useState(false);
+ const [currentRequestId, setCurrentRequestId] = useState(null);
+ const [isApproving, setIsApproving] = useState(false);
+ const [selectedRequest, setSelectedRequest] = useState(null);
+ const [showSlideOver, setShowSlideOver] = useState(false);
+
+ // 从Redux store获取权限申请列表状态
+ const {
+ results: permissionRequests,
+ status: fetchStatus,
+ error: fetchError,
+ page: currentPage,
+ page_size: pageSize,
+ total,
+ } = useSelector((state) => state.permissions.pending);
+
+ // 计算总页数
+ const totalPages = Math.ceil(total / pageSize) || 1;
+
+ // 从Redux store获取批准/拒绝操作的状态
+ const {
+ status: approveRejectStatus,
+ error: approveRejectError,
+ currentId: processingId,
+ } = useSelector((state) => state.permissions.approveReject);
+
+ // 获取待处理申请列表
+ useEffect(() => {
+ dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize }));
+ }, [dispatch, currentPage, pageSize]);
+
+ // 处理从通知中心跳转过来的请求
+ useEffect(() => {
+ if (location.state?.showRequestDetail) {
+ const { requestId, requestData } = location.state;
+ setSelectedRequest(requestData);
+ setShowSlideOver(true);
+ // 清除 location state
+ window.history.replaceState({}, document.title);
+ }
+ }, [location.state]);
+
+ // 监听批准/拒绝操作的状态变化
+ useEffect(() => {
+ if (approveRejectStatus === 'succeeded') {
+ dispatch(
+ showNotification({
+ message: isApproving ? '已批准申请' : '已拒绝申请',
+ type: 'success',
+ })
+ );
+ setShowResponseInput(false);
+ setCurrentRequestId(null);
+ setResponseMessage('');
+ setShowSlideOver(false);
+ setSelectedRequest(null);
+
+ // 重新获取申请列表
+ dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize }));
+
+ // 重置状态
+ dispatch(resetOperationStatus());
+ } else if (approveRejectStatus === 'failed') {
+ dispatch(
+ showNotification({
+ message: approveRejectError || '处理申请失败',
+ type: 'danger',
+ })
+ );
+ // 重置状态
+ dispatch(resetOperationStatus());
+ }
+ }, [approveRejectStatus, approveRejectError, dispatch, isApproving, currentPage, pageSize]);
+
+ // 打开回复输入框
+ const handleOpenResponseInput = (requestId, approving) => {
+ setCurrentRequestId(requestId);
+ setIsApproving(approving);
+ setShowResponseInput(true);
+ };
+
+ // 关闭回复输入框
+ const handleCloseResponseInput = () => {
+ setShowResponseInput(false);
+ setCurrentRequestId(null);
+ setResponseMessage('');
+ };
+
+ // 处理申请(批准或拒绝)
+ const handleProcessRequest = () => {
+ if (!currentRequestId) return;
+
+ const params = {
+ id: currentRequestId,
+ responseMessage,
+ };
+
+ if (isApproving) {
+ dispatch(approvePermissionThunk(params));
+ } else {
+ dispatch(rejectPermissionThunk(params));
+ }
+ };
+
+ // 处理行点击,显示滑动面板
+ const handleRowClick = (request) => {
+ setSelectedRequest(request);
+ setShowSlideOver(true);
+ };
+
+ // 关闭滑动面板
+ const handleCloseSlideOver = () => {
+ setShowSlideOver(false);
+ setTimeout(() => {
+ setSelectedRequest(null);
+ }, 300); // 等待动画结束后再清除选中的申请
+ };
+
+ // 直接处理申请(不显示弹窗)
+ const handleDirectProcess = (requestId, approve) => {
+ setCurrentRequestId(requestId);
+ setIsApproving(approve);
+
+ const params = {
+ id: requestId,
+ responseMessage: approve ? '已批准' : '已拒绝',
+ };
+
+ if (approve) {
+ dispatch(approvePermissionThunk(params));
+ } else {
+ dispatch(rejectPermissionThunk(params));
+ }
+ };
+
+ // 处理页码变化
+ const handlePageChange = (page) => {
+ dispatch(fetchPermissionsThunk({ page, page_size: pageSize }));
+ };
+
+ // 渲染分页控件
+ const renderPagination = () => {
+ if (totalPages <= 1) return null;
+
+ return (
+
+
+
+
+ {currentPage} / {totalPages}
+
+
+
+
+ );
+ };
+
+ // 根据状态渲染不同的状态标签
+ const renderStatusBadge = (status) => {
+ switch (status) {
+ case 'approved':
+ return (
+
+
+ 已批准
+
+ );
+ case 'rejected':
+ return (
+
+
+ 已拒绝
+
+ );
+ case 'pending':
+ return (
+
+ 待处理
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ // 渲染加载状态
+ if (fetchStatus === 'loading' && permissionRequests.length === 0) {
+ return (
+
+
+ 加载中...
+
+
加载权限申请...
+
+ );
+ }
+
+ // 渲染错误状态
+ if (fetchStatus === 'failed' && permissionRequests.length === 0) {
+ return (
+
+ {fetchError || '获取权限申请失败'}
+
+ );
+ }
+
+ // 渲染空状态
+ if (permissionRequests.length === 0) {
+ return (
+
+ 暂无权限申请记录
+
+ );
+ }
+
+ // 渲染申请列表
+ return (
+ <>
+
+
权限申请列表
+
+ {permissionRequests.filter((req) => req.status === 'pending').length}个待处理
+
+
+
+
+ {permissionRequests.map((request) => (
+
handleRowClick(request)}>
+
+
+
{request.applicant.name || request.applicant.username}
+ {request.applicant.department || '未分配部门'}
+
+
{new Date(request.created_at).toLocaleDateString()}
+
+
+
+
+
申请访问:{request.knowledge_base.name}
+
+
+ {request.permissions.can_edit ? (
+
+
+ 完全访问
+
+ ) : (
+ request.permissions.can_read && (
+
+
+ 只读访问
+
+ )
+ )}
+
+ {request.expires_at && (
+
+
+ 到期时间: {new Date(request.expires_at).toLocaleDateString()}
+
+
+ )}
+
+ {request.response_message && (
+
+ 审批意见: {request.response_message}
+
+ )}
+
+
+ {request.status === 'pending' ? (
+ <>
+
+
+ >
+ ) : (
+
+ {request.status === 'approved' ? '已批准' : '已拒绝'}
+ {request.updated_at && (
+
+ {new Date(request.updated_at).toLocaleDateString()}
+
+ )}
+
+ )}
+
+
+ ))}
+
+
+ {/* 分页控件 */}
+ {renderPagination()}
+
+ {/* 使用新的滑动面板组件 */}
+
handleOpenResponseInput(id, true)}
+ onReject={(id) => handleOpenResponseInput(id, false)}
+ processingId={processingId}
+ approveRejectStatus={approveRejectStatus}
+ isApproving={isApproving}
+ />
+
+ {/* 回复输入弹窗 */}
+ {showResponseInput && (
+
+
+
+
+
{isApproving ? '批准' : '拒绝'}申请
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/pages/Permissions/components/RequestDetailSlideOver.jsx b/src/pages/Permissions/components/RequestDetailSlideOver.jsx
new file mode 100644
index 0000000..7a7183b
--- /dev/null
+++ b/src/pages/Permissions/components/RequestDetailSlideOver.jsx
@@ -0,0 +1,178 @@
+import React from 'react';
+import SvgIcon from '../../../components/SvgIcon';
+
+export default function RequestDetailSlideOver({
+ show,
+ onClose,
+ request,
+ onApprove,
+ onReject,
+ processingId,
+ approveRejectStatus,
+ isApproving,
+}) {
+ if (!request) return null;
+
+ // 获取申请人信息
+ const applicantName = request.applicant?.name || request.applicant?.username || '未知用户';
+ const applicantDept = request.applicant?.department || '未分配部门';
+ const applicantInitial = applicantName.charAt(0);
+ const knowledgeBaseName = request.knowledge_base?.name || '未知知识库';
+ const knowledgeBaseId = request.knowledge_base?.id || '';
+ const knowledgeBaseType = request.knowledge_base?.type || '';
+
+ return (
+ <>
+
+
+
+
+
申请详情
+
+
+
+
+
申请人信息
+
+
{applicantInitial}
+
+
{applicantName}
+
{applicantDept}
+
+
+
+
+
+
知识库信息
+
+ 名称: {knowledgeBaseName}
+
+
+ ID: {knowledgeBaseId}
+
+ {knowledgeBaseType && (
+
+ 类型: {knowledgeBaseType}
+
+ )}
+
+
+
+
申请权限
+
+ {request.permissions?.can_read && !request.permissions?.can_edit && (
+
+
+ 只读
+
+ )}
+ {request.permissions?.can_edit && (
+
+
+ 编辑
+
+ )}
+ {request.permissions?.can_delete && (
+
+
+ 删除
+
+ )}
+
+
+
+
+
申请时间
+
{new Date(request.created_at || Date.now()).toLocaleString()}
+
+
+ {request.expires_at && (
+
+
到期时间
+
{new Date(request.expires_at).toLocaleString()}
+
+ )}
+
+ {request.status && (
+
+
申请状态
+
+ {request.status === 'pending' ? (
+ 待处理
+ ) : request.status === 'approved' ? (
+ 已批准
+ ) : (
+ 已拒绝
+ )}
+
+
+ )}
+
+ {request.approver && (
+
+
审批人
+
+ {request.approver.name || request.approver.username} ({request.approver.department || '未分配部门'})
+
+
+ )}
+
+ {request.response_message && (
+
+
审批意见
+
{request.response_message}
+
+ )}
+
+
+
申请理由
+
+ {request.reason || '无申请理由'}
+
+
+
+ {request.status === 'pending' && (
+
+
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/pages/Permissions/components/UserPermissionDetails.jsx b/src/pages/Permissions/components/UserPermissionDetails.jsx
new file mode 100644
index 0000000..5f314fd
--- /dev/null
+++ b/src/pages/Permissions/components/UserPermissionDetails.jsx
@@ -0,0 +1,300 @@
+import React, { useState, useEffect } from 'react';
+import { get, post } from '../../../services/api';
+
+export default function UserPermissionDetails({ user, onClose, onSave }) {
+ const [updatedPermissions, setUpdatedPermissions] = useState({});
+ const [userPermissions, setUserPermissions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [savingPermissions, setSavingPermissions] = useState(false);
+ const [successMessage, setSuccessMessage] = useState(null);
+
+ useEffect(() => {
+ // If we already have the permissions data in the user object, use it
+ if (user.permissions && Array.isArray(user.permissions)) {
+ setUserPermissions(user.permissions);
+ setLoading(false);
+ }
+ }, [user]);
+
+ const handlePermissionChange = (knowledgeBaseId, newPermissionType) => {
+ setUpdatedPermissions((prev) => ({
+ ...prev,
+ [knowledgeBaseId]: newPermissionType,
+ }));
+ };
+
+ const handleSave = async () => {
+ setSavingPermissions(true);
+ setError(null);
+ setSuccessMessage(null);
+
+ try {
+ const permissionUpdates = Object.entries(updatedPermissions).map(
+ async ([knowledgeBaseId, permissionType]) => {
+ const permissions = {
+ can_read: permissionType !== 'none',
+ can_edit: ['edit', 'admin'].includes(permissionType),
+ can_delete: permissionType === 'admin',
+ };
+
+ const requestBody = {
+ user_id: user.user_info.id,
+ knowledge_base_id: knowledgeBaseId,
+ permissions: permissions,
+ // Optional expiration date - can be added if needed
+ // expires_at: "2025-12-31T23:59:59Z"
+ };
+
+ try {
+ const response = await post('/permissions/update_permission/', requestBody);
+ return response;
+ } catch (err) {
+ throw new Error(`更新知识库 ${knowledgeBaseId} 权限失败: ${err.message || '未知错误'}`);
+ }
+ }
+ );
+
+ await Promise.all(permissionUpdates);
+
+ setSuccessMessage('权限更新成功');
+ // Reset updated permissions
+ setUpdatedPermissions({});
+
+ // Notify parent component if needed
+ if (onSave) {
+ onSave(user.user_info.id);
+ }
+ } catch (err) {
+ setError(err.message || '更新权限时发生错误');
+ } finally {
+ setSavingPermissions(false);
+ }
+ };
+
+ // 获取权限类型的显示文本
+ const getPermissionTypeText = (permissionType) => {
+ switch (permissionType) {
+ case 'none':
+ return '无权限';
+ case 'read':
+ return '只读访问';
+ case 'edit':
+ return '编辑权限';
+ case 'admin':
+ return '管理权限';
+ default:
+ return '未知权限';
+ }
+ };
+
+ // 获取权限类型的值
+ const getPermissionType = (permission) => {
+ if (!permission) return 'none';
+ if (permission.can_delete) return 'admin';
+ if (permission.can_edit) return 'edit';
+ if (permission.can_read) return 'read';
+ return 'none';
+ };
+
+ return (
+
+
+
+
+
{user.user_info.name} 的权限详情
+
+
+
+ {successMessage && (
+
+ {successMessage}
+
+
+ )}
+
+ {loading ? (
+
+
+ 加载中...
+
+
加载权限详情...
+
+ ) : error ? (
+
+
+ {error}
+
+ {userPermissions.length > 0 && (
+
+
+
+
+ 知识库名称 |
+ 所属部门 |
+ 当前权限 |
+ 最后访问时间 |
+ 操作 |
+
+
+
+ {userPermissions.map((item) => {
+ const currentPermissionType = getPermissionType(item.permissions);
+ const updatedPermissionType =
+ updatedPermissions[item.knowledge_base.id] ||
+ currentPermissionType;
+
+ return (
+
+ {item.knowledge_base.name} |
+ {item.knowledge_base.department || '未指定'} |
+
+
+ {getPermissionTypeText(currentPermissionType)}
+
+ |
+
+ {item.granted_at
+ ? new Date(item.granted_at).toLocaleString()
+ : '从未访问'}
+ |
+
+
+ |
+
+ );
+ })}
+
+
+
+ )}
+
+ ) : userPermissions.length === 0 ? (
+
+ 该用户暂无任何知识库权限
+
+ ) : (
+
+
+
+
+ 知识库名称 |
+ 所属部门 |
+ 当前权限 |
+ 授权时间 |
+ 操作 |
+
+
+
+ {userPermissions.map((item) => {
+ const currentPermissionType = getPermissionType(item.permissions);
+ const updatedPermissionType =
+ updatedPermissions[item.knowledge_base.id] || currentPermissionType;
+
+ return (
+
+ {item.knowledge_base.name} |
+ {item.knowledge_base.department || '未指定'} |
+
+
+ {getPermissionTypeText(currentPermissionType)}
+
+ |
+
+ {item.granted_at
+ ? new Date(item.granted_at).toLocaleString()
+ : '未记录'}
+ |
+
+
+ |
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Permissions/components/UserPermissions.css b/src/pages/Permissions/components/UserPermissions.css
new file mode 100644
index 0000000..1e1dd18
--- /dev/null
+++ b/src/pages/Permissions/components/UserPermissions.css
@@ -0,0 +1,210 @@
+.search-box {
+ position: relative;
+}
+
+.search-input {
+ padding: 8px 12px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ width: 240px;
+ font-size: 14px;
+ outline: none;
+ transition: all 0.3s;
+}
+
+.search-input:focus {
+ border-color: #1890ff;
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+}
+
+.user-permissions-table {
+ background-color: #fff;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.table-header {
+ display: flex;
+ background-color: #fafafa;
+ border-bottom: 1px solid #f0f0f0;
+ padding: 12px 16px;
+ font-weight: 600;
+ color: #333;
+}
+
+.header-cell {
+ flex: 1;
+}
+
+.header-cell:first-child {
+ flex: 1.5;
+}
+
+.table-row {
+ display: flex;
+ padding: 12px 16px;
+ border-bottom: 1px solid #f0f0f0;
+ transition: background-color 0.3s;
+}
+
+.table-row:hover {
+ background-color: #fafafa;
+}
+
+.table-row:last-child {
+ border-bottom: none;
+}
+
+.cell {
+ flex: 1;
+ display: flex;
+ align-items: center;
+}
+
+.cell:first-child {
+ flex: 1.5;
+}
+
+.user-cell {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.avatar-placeholder {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background-color: #1890ff;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ font-size: 16px;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.user-name {
+ font-weight: 500;
+ color: #333;
+}
+
+.user-username {
+ font-size: 12px;
+ color: #999;
+}
+
+.permission-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.permission-badge {
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.permission-badge.read {
+ background-color: #e6f7ff;
+ color: #1890ff;
+ border: 1px solid #91d5ff;
+}
+
+.permission-badge.edit {
+ background-color: #f6ffed;
+ color: #52c41a;
+ border: 1px solid #b7eb8f;
+}
+
+.permission-badge.admin {
+ background-color: #fff2f0;
+ color: #ff4d4f;
+ border: 1px solid #ffccc7;
+}
+
+.action-cell {
+ justify-content: flex-end;
+}
+
+.btn-details {
+ background-color: transparent;
+ border: 1px solid #1890ff;
+ color: #1890ff;
+ padding: 4px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.2s;
+}
+
+.btn-details:hover {
+ background-color: #e6f7ff;
+}
+
+/* 分页控件样式 */
+.pagination-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 20px;
+ padding: 10px 0;
+}
+
+.pagination-button {
+ background-color: #fff;
+ border: 1px solid #d9d9d9;
+ color: #333;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.2s;
+}
+
+.pagination-button:hover:not(:disabled) {
+ border-color: #1890ff;
+ color: #1890ff;
+}
+
+.pagination-button:disabled {
+ color: #d9d9d9;
+ cursor: not-allowed;
+}
+
+.pagination-info {
+ margin: 0 15px;
+ color: #666;
+ font-size: 14px;
+}
+
+/* 页面大小选择器样式 */
+.page-size-selector {
+ margin-left: 20px;
+ display: flex;
+ align-items: center;
+}
+
+.page-size-select {
+ padding: 5px 10px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ background-color: #fff;
+ font-size: 14px;
+ color: #333;
+ cursor: pointer;
+ outline: none;
+ transition: all 0.3s;
+}
+
+.page-size-select:hover, .page-size-select:focus {
+ border-color: #1890ff;
+}
\ No newline at end of file
diff --git a/src/pages/Permissions/components/UserPermissions.jsx b/src/pages/Permissions/components/UserPermissions.jsx
new file mode 100644
index 0000000..42dad52
--- /dev/null
+++ b/src/pages/Permissions/components/UserPermissions.jsx
@@ -0,0 +1,373 @@
+import React, { useState, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchAllUserPermissions, updateUserPermissions } from '../../../store/permissions/permissions.thunks';
+import UserPermissionDetails from './UserPermissionDetails';
+import './UserPermissions.css';
+import SvgIcon from '../../../components/SvgIcon';
+
+// 每页显示选项
+const PAGE_SIZE_OPTIONS = [5, 10, 15, 20, 50, 100];
+
+export default function UserPermissions() {
+ const dispatch = useDispatch();
+ const [selectedUser, setSelectedUser] = useState(null);
+ const [showDetailsModal, setShowDetailsModal] = useState(false);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all'); // 'all', 'pending', 'approved', 'rejected'
+
+ // 分页状态
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+
+ // 从 Redux store 获取用户列表和状态
+ const {
+ results: users,
+ status: loading,
+ error,
+ total,
+ page,
+ page_size,
+ } = useSelector((state) => state.permissions.allUsersPermissions);
+
+ // 获取用户列表
+ useEffect(() => {
+ dispatch(fetchAllUserPermissions({ page: currentPage, page_size: pageSize }));
+ }, [dispatch, currentPage, pageSize]);
+
+ // 计算总页数
+ const totalPages = Math.ceil(total / page_size);
+
+ const handleOpenDetailsModal = (data) => {
+ setSelectedUser(data);
+ setShowDetailsModal(true);
+ };
+
+ const handleCloseDetailsModal = () => {
+ setSelectedUser(null);
+ setShowDetailsModal(false);
+ };
+
+ const handleSavePermissions = async (userId) => {
+ try {
+ // Permission updates are now handled directly in the UserPermissionDetails component
+ // Just refresh the users list to reflect the updated permissions
+ handleCloseDetailsModal();
+ dispatch(fetchAllUserPermissions({ page: currentPage, page_size: pageSize }));
+ } catch (error) {
+ console.error('更新权限失败:', error);
+ }
+ };
+
+ const handleSearchChange = (e) => {
+ setSearchTerm(e.target.value);
+ setCurrentPage(1); // 重置到第一页
+ };
+
+ const handleStatusFilterChange = (e) => {
+ setStatusFilter(e.target.value);
+ setCurrentPage(1); // 重置到第一页
+ };
+
+ const handlePageChange = (page) => {
+ if (page > 0 && page <= totalPages) {
+ setCurrentPage(page);
+ }
+ };
+
+ const handlePageSizeChange = (e) => {
+ const newPageSize = parseInt(e.target.value);
+ setPageSize(newPageSize);
+ setCurrentPage(1); // 重置到第一页
+ };
+
+ // 过滤用户列表
+ const getFilteredUsers = () => {
+ let filtered = users;
+
+ // 应用状态过滤
+ if (statusFilter !== 'all') {
+ filtered = filtered.filter((user) =>
+ user.permissions.some((permission) => permission.status === statusFilter)
+ );
+ }
+
+ // 应用搜索过滤
+ if (searchTerm.trim()) {
+ filtered = filtered.filter(
+ (user) =>
+ user.user_info?.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ user.user_info?.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ user.user_info?.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ user.user_info?.role.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ }
+
+ return filtered;
+ };
+
+ const filteredUsers = getFilteredUsers();
+
+ const renderPagination = () => {
+ const pageNumbers = [];
+ const ellipsis = (
+
+ ...
+
+ );
+
+ let startPage = Math.max(1, currentPage - 2);
+ let endPage = Math.min(totalPages, startPage + 4);
+
+ if (endPage - startPage < 4) {
+ startPage = Math.max(1, endPage - 4);
+ }
+
+ // 总是显示第一页
+ if (startPage > 1) {
+ pageNumbers.push(
+
+
+
+ );
+
+ // 如果第一页和起始页之间有间隔,显示省略号
+ if (startPage > 2) {
+ pageNumbers.push(ellipsis);
+ }
+ }
+
+ // 添加页码按钮
+ for (let i = startPage; i <= endPage; i++) {
+ pageNumbers.push(
+
+
+
+ );
+ }
+
+ // 如果末页和最后一页之间有间隔,显示省略号
+ if (endPage < totalPages - 1) {
+ pageNumbers.push(ellipsis);
+ }
+
+ // 如果末页不是最后一页,显示最后一页
+ if (endPage < totalPages) {
+ pageNumbers.push(
+
+
+
+ );
+ }
+
+ return (
+
+
+ 每页显示:
+
+
+ 总计 {total} 条记录
+
+
+
+
+
+ );
+ };
+
+ return (
+ <>
+
+
用户权限管理
+
+
+ <>
+
+
+
+
+
+
+
+ {loading === 'loading' ? (
+
+
+ 加载中...
+
+
加载用户权限列表...
+
+ ) : error ? (
+
+ {error}
+
+ ) : (
+ <>
+
+
+
+
+ ID |
+ 用户名 |
+ 姓名 |
+ 部门 |
+ 角色 |
+ 权限类型 |
+ 操作 |
+
+
+
+ {filteredUsers.length > 0 ? (
+ filteredUsers.map((userPermission) => (
+
+ {userPermission.user_info?.id} |
+
+ {userPermission.user_info?.username || 'N/A'}
+ |
+
+ {userPermission.user_info?.name || 'N/A'}
+ |
+
+ {userPermission.user_info?.department || 'N/A'}
+ |
+
+
+ {userPermission.user_info?.role === 'admin'
+ ? '管理员'
+ : userPermission.user_info?.role === 'leader'
+ ? '组长'
+ : '成员'}
+
+ |
+
+
+ {userPermission.stats?.by_permission?.full_access > 0 && (
+
+ 完全访问:{' '}
+ {userPermission.stats?.by_permission?.full_access}
+
+ )}
+
+ {userPermission.stats?.by_permission?.read_only > 0 && (
+
+ 只读访问:{' '}
+ {userPermission.stats?.by_permission?.read_only}
+
+ )}
+
+ {userPermission.stats?.by_permission?.read_write > 0 && (
+
+ 读写权限:{' '}
+ {userPermission.stats?.by_permission?.read_write}
+
+ )}
+
+ |
+
+
+ |
+
+ ))
+ ) : (
+
+
+ 无匹配的用户记录
+ |
+
+ )}
+
+
+
+
+ {totalPages > 1 && renderPagination()}
+ >
+ )}
+
+ {showDetailsModal && selectedUser && (
+
+ )}
+ >
+ >
+ );
+}
diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.jsx
new file mode 100644
index 0000000..132f965
--- /dev/null
+++ b/src/pages/auth/Login.jsx
@@ -0,0 +1,120 @@
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { Link, useNavigate } from 'react-router-dom';
+import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
+import { showNotification } from '../../store/notification.slice';
+
+export default function Login() {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const [username, setUsername] = useState('leader2');
+ const [password, setPassword] = useState('leader123');
+ const [errors, setErrors] = useState({});
+ const [submitted, setSubmitted] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { user } = useSelector((state) => state.auth);
+
+ useEffect(() => {
+ handleCheckAuth();
+ }, [dispatch]);
+
+ const handleCheckAuth = async () => {
+ console.log('login page handleCheckAuth');
+ try {
+ await dispatch(checkAuthThunk()).unwrap();
+ if (user) navigate('/');
+ } catch (error) {
+ // 检查登录状态失败,不需要显示通知
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!username) {
+ newErrors.username = '请输入用户名';
+ }
+ if (!password) {
+ newErrors.password = '请输入密码';
+ } else if (password.length < 6) {
+ newErrors.password = '密码长度不能少于6个字符';
+ }
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setSubmitted(true);
+
+ if (validateForm()) {
+ setIsLoading(true);
+ try {
+ await dispatch(loginThunk({ username, password })).unwrap();
+ navigate('/');
+ } catch (error) {
+ // 登录失败的错误通知已在thunk中处理
+ console.error('Login failed:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ return (
+
+
OOIN 智能知识库
+
+
+ 没有账号?去注册
+
+
+ );
+}
diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx
new file mode 100644
index 0000000..b57e977
--- /dev/null
+++ b/src/pages/auth/Signup.jsx
@@ -0,0 +1,270 @@
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { Link, useNavigate } from 'react-router-dom';
+import { checkAuthThunk, signupThunk } from '../../store/auth/auth.thunk';
+
+// 部门和组别映射关系
+const departmentGroups = {
+ '技术部': ['开发组', '测试组', '运维组'],
+ '产品部': ['产品规划组', '用户研究组', '交互设计组', '项目管理组'],
+ '市场部': ['品牌推广组', '市场调研组', '客户关系组', '社交媒体组'],
+ '行政部': ['人事组', '财务组', '行政管理组', '后勤组']
+};
+
+export default function Signup() {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ password: '',
+ name: '',
+ role: 'member',
+ department: '',
+ group: '',
+ });
+ const [errors, setErrors] = useState({});
+ const [submitted, setSubmitted] = useState(false);
+ const [availableGroups, setAvailableGroups] = useState([]);
+
+ const { user, loading } = useSelector((state) => state.auth);
+
+ useEffect(() => {
+ handleCheckAuth();
+ }, [dispatch]);
+
+ // 当部门变化时,更新可选的组别
+ useEffect(() => {
+ if (formData.department && departmentGroups[formData.department]) {
+ setAvailableGroups(departmentGroups[formData.department]);
+ // 如果已选择的组别不在新部门的选项中,则重置组别
+ if (!departmentGroups[formData.department].includes(formData.group)) {
+ setFormData(prev => ({
+ ...prev,
+ group: ''
+ }));
+ }
+ } else {
+ setAvailableGroups([]);
+ setFormData(prev => ({
+ ...prev,
+ group: ''
+ }));
+ }
+ }, [formData.department]);
+
+ const handleCheckAuth = async () => {
+ console.log('signup page handleCheckAuth');
+ try {
+ await dispatch(checkAuthThunk()).unwrap();
+ if (user) navigate('/');
+ } catch (error) {}
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData({
+ ...formData,
+ [name]: value,
+ });
+
+ // 清除对应的错误信息
+ if (errors[name]) {
+ setErrors({
+ ...errors,
+ [name]: '',
+ });
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.username) {
+ newErrors.username = 'Username is required';
+ }
+
+ if (!formData.email) {
+ newErrors.email = 'Email is required';
+ } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formData.email)) {
+ newErrors.email = 'Invalid email address';
+ }
+
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters';
+ }
+
+ if (!formData.name) {
+ newErrors.name = 'Name is required';
+ }
+
+ if (!formData.department) {
+ newErrors.department = '请选择部门';
+ }
+
+ if (!formData.group) {
+ newErrors.group = '请选择组别';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setSubmitted(true);
+
+ if (validateForm()) {
+ console.log('Form submitted successfully!');
+ console.log('Registration data:', formData);
+ try {
+ await dispatch(signupThunk(formData)).unwrap();
+ navigate('/login');
+ } catch (error) {
+ console.error('Signup failed:', error);
+ }
+ }
+ };
+
+ return (
+
+
OOIN 智能知识库
+
+
+ 已有账号?立即登录
+
+
+ );
+}
diff --git a/src/router.jsx b/src/router.jsx
deleted file mode 100644
index 125c0a5..0000000
--- a/src/router.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React, { Suspense } from 'react';
-import { Route, Routes } from 'react-router-dom';
-import Mainlayout from './layouts/Mainlayout';
-import KnowledgeBase from './pages/KnowledgeBase/KnowledgeBase';
-import Loading from './components/Loading';
-
-function AppRouter() {
- return (
- // }>
-
-
-
-
- }
- />
-
- //
- );
-}
-export default AppRouter;
diff --git a/src/router/protectedRoute.jsx b/src/router/protectedRoute.jsx
new file mode 100644
index 0000000..ac4c9d8
--- /dev/null
+++ b/src/router/protectedRoute.jsx
@@ -0,0 +1,12 @@
+import { useEffect } from 'react';
+import { useSelector } from 'react-redux';
+import { Navigate, Outlet } from 'react-router-dom';
+
+function ProtectedRoute() {
+ const { user } = useSelector((state) => state.auth);
+
+ return
+ // return !!user ? : ;
+}
+
+export default ProtectedRoute;
diff --git a/src/router/router.jsx b/src/router/router.jsx
new file mode 100644
index 0000000..7e4f11a
--- /dev/null
+++ b/src/router/router.jsx
@@ -0,0 +1,92 @@
+import React, { Suspense } from 'react';
+import { Navigate, Route, Routes } from 'react-router-dom';
+import Mainlayout from '../layouts/Mainlayout';
+import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
+import KnowledgeBaseDetail from '../pages/KnowledgeBase/Detail/KnowledgeBaseDetail';
+import Chat from '../pages/Chat/Chat';
+import PermissionsPage from '../pages/Permissions/PermissionsPage';
+import Loading from '../components/Loading';
+import Login from '../pages/Auth/Login';
+import Signup from '../pages/Auth/Signup';
+import ProtectedRoute from './protectedRoute';
+import { useSelector } from 'react-redux';
+import NotificationSnackbar from '../components/NotificationSnackbar';
+
+function AppRouter() {
+ const { user } = useSelector((state) => state.auth);
+
+ // 检查用户是否有管理权限(leader 或 admin)
+ const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin');
+
+ return (
+ }>
+
+
+
+ }>
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ {/* 权限管理页面路由 - 仅对 leader 或 admin 角色可见 */}
+
+
+
+ }
+ />
+
+ } />
+ } />
+ } />
+
+
+ );
+}
+export default AppRouter;
diff --git a/src/services/api.js b/src/services/api.js
index 1f003dc..13a9291 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -1,14 +1,27 @@
import axios from 'axios';
+import CryptoJS from 'crypto-js';
+import { mockGet, mockPost, mockPut, mockDelete } from './mockApi';
+
+const secretKey = import.meta.env.VITE_SECRETKEY;
+
+// API连接状态
+let isServerDown = false;
+let hasCheckedServer = false;
// Create Axios instance with base URL
const api = axios.create({
- baseURL: 'api',
+ baseURL: '/api',
withCredentials: true, // Include cookies if needed
});
// Request Interceptor
api.interceptors.request.use(
(config) => {
+ const encryptedToken = sessionStorage.getItem('token') || '';
+ if (encryptedToken) {
+ const decryptedToken = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8);
+ config.headers.Authorization = `Token ${decryptedToken}`;
+ }
return config;
},
(error) => {
@@ -20,14 +33,27 @@ api.interceptors.request.use(
// Response Interceptor
api.interceptors.response.use(
(response) => {
+ // 如果成功收到响应,表示服务器正常工作
+ if (!hasCheckedServer) {
+ console.log('Server is up and running');
+ isServerDown = false;
+ hasCheckedServer = true;
+ }
return response;
},
(error) => {
+ // 处理服务器无法连接的情况
+ if (!error.response || error.code === 'ECONNABORTED' || error.message.includes('Network Error')) {
+ console.error('Server appears to be down. Switching to mock data.');
+ isServerDown = true;
+ hasCheckedServer = true;
+ }
+
// Handle errors in the response
if (error.response) {
// monitor /verify
- if (error.response.status === 401 && error.config.url === '/check-token') {
- if (window.location.pathname !== '/login' && window.location.pathname !== '/register') {
+ if (error.response.status === 401 && error.config.url === '/auth/verify-token/') {
+ if (window.location.pathname !== '/login' && window.location.pathname !== '/signup') {
window.location.href = '/login';
}
}
@@ -48,44 +74,161 @@ api.interceptors.response.use(
}
);
-// Define common HTTP methods
+// 检查服务器状态
+export const checkServerStatus = async () => {
+ try {
+ // await api.get('/health-check', { timeout: 3000 });
+ isServerDown = false;
+ hasCheckedServer = true;
+ console.log('Server connection established');
+ return true;
+ } catch (error) {
+ isServerDown = true;
+ hasCheckedServer = true;
+ console.error('Server connection failed, using mock data');
+ return false;
+ }
+};
+
+// 初始检查服务器状态
+checkServerStatus();
+
+// Define common HTTP methods with fallback to mock API
const get = async (url, params = {}) => {
- const res = await api.get(url, { params });
- return res.data;
+ try {
+ if (isServerDown) {
+ console.log(`[MOCK MODE] GET ${url}`);
+ return await mockGet(url, params);
+ }
+
+ const res = await api.get(url, { ...params });
+ return res.data;
+ } catch (error) {
+ if (!hasCheckedServer || (error.request && !error.response)) {
+ console.log(`Failed to connect to server. Falling back to mock API for GET ${url}`);
+ return await mockGet(url, params);
+ }
+ throw error;
+ }
};
-// Handle POST requests for JSON data
+// Handle POST requests for JSON data with fallback to mock API
const post = async (url, data, isMultipart = false) => {
- const headers = isMultipart
- ? { 'Content-Type': 'multipart/form-data' } // For file uploads
- : { 'Content-Type': 'application/json' }; // For JSON data
+ try {
+ if (isServerDown) {
+ console.log(`[MOCK MODE] POST ${url}`);
+ return await mockPost(url, data);
+ }
- const res = await api.post(url, data, { headers });
- return res.data;
+ const headers = isMultipart
+ ? { 'Content-Type': 'multipart/form-data' } // For file uploads
+ : { 'Content-Type': 'application/json' }; // For JSON data
+
+ const res = await api.post(url, data, { headers });
+ return res.data;
+ } catch (error) {
+ if (!hasCheckedServer || (error.request && !error.response)) {
+ console.log(`Failed to connect to server. Falling back to mock API for POST ${url}`);
+ return await mockPost(url, data);
+ }
+ throw error;
+ }
};
-// Handle PUT requests
+// Handle PUT requests with fallback to mock API
const put = async (url, data) => {
- const res = await api.put(url, data, {
- headers: { 'Content-Type': 'application/json' },
- });
- return res.data;
+ try {
+ if (isServerDown) {
+ console.log(`[MOCK MODE] PUT ${url}`);
+ return await mockPut(url, data);
+ }
+
+ const res = await api.put(url, data, {
+ headers: { 'Content-Type': 'application/json' },
+ });
+ return res.data;
+ } catch (error) {
+ if (!hasCheckedServer || (error.request && !error.response)) {
+ console.log(`Failed to connect to server. Falling back to mock API for PUT ${url}`);
+ return await mockPut(url, data);
+ }
+ throw error;
+ }
};
-// Handle DELETE requests
+// Handle DELETE requests with fallback to mock API
const del = async (url) => {
- const res = await api.delete(url);
- return res.data;
+ try {
+ if (isServerDown) {
+ console.log(`[MOCK MODE] DELETE ${url}`);
+ return await mockDelete(url);
+ }
+
+ const res = await api.delete(url);
+ return res.data;
+ } catch (error) {
+ if (!hasCheckedServer || (error.request && !error.response)) {
+ console.log(`Failed to connect to server. Falling back to mock API for DELETE ${url}`);
+ return await mockDelete(url);
+ }
+ throw error;
+ }
};
const upload = async (url, data) => {
- const axiosInstance = await axios.create({
- baseURL: '/api',
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- });
- const res = await axiosInstance.post(url, data);
- return res.data;
+ try {
+ if (isServerDown) {
+ console.log(`[MOCK MODE] Upload ${url}`);
+ return await mockPost(url, data, true);
+ }
+
+ const axiosInstance = await axios.create({
+ baseURL: '/api',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ const res = await axiosInstance.post(url, data);
+ return res.data;
+ } catch (error) {
+ if (!hasCheckedServer || (error.request && !error.response)) {
+ console.log(`Failed to connect to server. Falling back to mock API for Upload ${url}`);
+ return await mockPost(url, data, true);
+ }
+ throw error;
+ }
};
+
+// 手动切换到模拟API(为调试目的)
+export const switchToMockApi = () => {
+ isServerDown = true;
+ hasCheckedServer = true;
+ console.log('Manually switched to mock API');
+};
+
+// 手动切换回真实API
+export const switchToRealApi = async () => {
+ // 重新检查服务器状态
+ const isServerUp = await checkServerStatus();
+ console.log(isServerUp ? 'Switched back to real API' : 'Server still down, continuing with mock API');
+ return isServerUp;
+};
+
+// 权限相关API
+export const applyPermission = (data) => {
+ return post('/permissions/', data);
+};
+
+export const updatePermission = (data) => {
+ return post('/permissions/update_permission/', data);
+};
+
+export const approvePermission = (permissionId) => {
+ return post(`/permissions/approve_permission/${permissionId}`);
+};
+
+export const rejectPermission = (permissionId) => {
+ return post(`/permissions/reject_permission/${permissionId}`);
+};
+
export { get, post, put, del, upload };
diff --git a/src/services/mockApi.js b/src/services/mockApi.js
new file mode 100644
index 0000000..46c9355
--- /dev/null
+++ b/src/services/mockApi.js
@@ -0,0 +1,990 @@
+// Mock API service for development without backend
+import { v4 as uuidv4 } from 'uuid';
+
+// Mock data for knowledge bases
+const mockKnowledgeBases = [
+ {
+ id: uuidv4(),
+ user_id: 'user-001',
+ name: 'Frontend Development',
+ desc: 'Resources and guides for frontend development including React, Vue, and Angular',
+ type: 'private',
+ department: '研发部',
+ group: '前端开发组',
+ documents: [],
+ char_length: 0,
+ document_count: 0,
+ external_id: uuidv4(),
+ create_time: '2024-02-26T08:30:00Z',
+ update_time: '2024-02-26T14:45:00Z',
+ permissions: {
+ can_read: true,
+ can_edit: true,
+ can_delete: false,
+ },
+ },
+ {
+ id: uuidv4(),
+ user_id: 'user-001',
+ name: 'Backend Technologies',
+ desc: 'Information about backend frameworks, databases, and server configurations',
+ type: 'private',
+ department: '研发部',
+ group: '后端开发组',
+ documents: [],
+ char_length: 0,
+ document_count: 0,
+ external_id: uuidv4(),
+ create_time: '2024-02-25T10:15:00Z',
+ update_time: '2024-02-26T09:20:00Z',
+ permissions: {
+ can_read: true,
+ can_edit: true,
+ can_delete: false,
+ },
+ },
+ {
+ id: 'kb-003',
+ name: 'DevOps Practices',
+ description: 'Best practices for CI/CD, containerization, and cloud deployment',
+ created_at: '2023-11-12T15:45:00Z',
+ updated_at: '2024-02-05T11:30:00Z',
+ create_time: '2023-11-12T15:45:00Z',
+ update_time: '2024-02-05T11:30:00Z',
+ type: 'public',
+ owner: {
+ id: 'user-002',
+ username: 'janedoe',
+ email: 'jane@example.com',
+ },
+ document_count: 18,
+ tags: ['docker', 'kubernetes', 'aws'],
+ permissions: {
+ can_edit: false,
+ can_read: true,
+ },
+ },
+ {
+ id: 'kb-004',
+ name: 'Machine Learning Fundamentals',
+ description: 'Introduction to machine learning concepts, algorithms, and frameworks',
+ created_at: '2023-08-20T09:00:00Z',
+ updated_at: '2024-01-25T16:15:00Z',
+ create_time: '2023-08-20T09:00:00Z',
+ update_time: '2024-01-25T16:15:00Z',
+ type: 'public',
+ owner: {
+ id: 'user-003',
+ username: 'alexsmith',
+ email: 'alex@example.com',
+ },
+ document_count: 30,
+ tags: ['ml', 'python', 'tensorflow'],
+ permissions: {
+ can_edit: false,
+ can_read: true,
+ },
+ },
+ {
+ id: 'kb-005',
+ name: 'UI/UX Design Principles',
+ description: 'Guidelines for creating effective and user-friendly interfaces',
+ created_at: '2023-12-01T13:20:00Z',
+ updated_at: '2024-02-15T10:45:00Z',
+ create_time: '2023-12-01T13:20:00Z',
+ update_time: '2024-02-15T10:45:00Z',
+ type: 'private',
+ owner: {
+ id: 'user-002',
+ username: 'janedoe',
+ email: 'jane@example.com',
+ },
+ document_count: 12,
+ tags: ['design', 'ui', 'ux'],
+ permissions: {
+ can_edit: true,
+ can_read: true,
+ },
+ },
+ {
+ id: 'kb-006',
+ name: 'Mobile App Development',
+ description: 'Resources for iOS, Android, and cross-platform mobile development',
+ created_at: '2023-10-25T11:10:00Z',
+ updated_at: '2024-01-30T14:00:00Z',
+ create_time: '2023-10-25T11:10:00Z',
+ update_time: '2024-01-30T14:00:00Z',
+ type: 'private',
+ owner: {
+ id: 'user-001',
+ username: 'johndoe',
+ email: 'john@example.com',
+ },
+ document_count: 20,
+ tags: ['mobile', 'react-native', 'flutter'],
+ permissions: {
+ can_edit: true,
+ can_read: true,
+ },
+ },
+ {
+ id: 'kb-007',
+ name: 'Cybersecurity Best Practices',
+ description: 'Guidelines for securing applications, networks, and data',
+ created_at: '2023-09-18T14:30:00Z',
+ updated_at: '2024-02-10T09:15:00Z',
+ create_time: '2023-09-18T14:30:00Z',
+ update_time: '2024-02-10T09:15:00Z',
+ type: 'private',
+ owner: {
+ id: 'user-004',
+ username: 'sarahwilson',
+ email: 'sarah@example.com',
+ },
+ document_count: 25,
+ tags: ['security', 'encryption', 'authentication'],
+ permissions: {
+ can_edit: false,
+ can_read: false,
+ },
+ },
+ {
+ id: 'kb-008',
+ name: 'Data Science Toolkit',
+ description: 'Tools and techniques for data analysis, visualization, and modeling',
+ created_at: '2023-11-05T10:00:00Z',
+ updated_at: '2024-01-20T15:30:00Z',
+ create_time: '2023-11-05T10:00:00Z',
+ update_time: '2024-01-20T15:30:00Z',
+ type: 'public',
+ owner: {
+ id: 'user-003',
+ username: 'alexsmith',
+ email: 'alex@example.com',
+ },
+ document_count: 28,
+ tags: ['data-science', 'python', 'visualization'],
+ permissions: {
+ can_edit: false,
+ can_read: true,
+ },
+ },
+];
+
+// In-memory store for CRUD operations
+let knowledgeBases = [...mockKnowledgeBases];
+
+// Mock user data for authentication
+const mockUsers = [
+ {
+ id: 'user-001',
+ username: 'leader2',
+ password: 'leader123', // 在实际应用中不应该存储明文密码
+ email: 'admin@example.com',
+ name: '管理员',
+ department: '研发部',
+ group: '前端开发组',
+ role: 'admin',
+ avatar: null,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ },
+ {
+ id: 'user-002',
+ username: 'user',
+ password: 'user123', // 在实际应用中不应该存储明文密码
+ email: 'user@example.com',
+ name: '普通用户',
+ department: '市场部',
+ group: '市场组',
+ role: 'user',
+ avatar: null,
+ created_at: '2024-01-02T00:00:00Z',
+ updated_at: '2024-01-02T00:00:00Z',
+ },
+];
+
+// Helper function for pagination
+const paginate = (array, page_size, page) => {
+ const startIndex = (page - 1) * page_size;
+ const endIndex = startIndex + page_size;
+ const items = array.slice(startIndex, endIndex);
+
+ return {
+ items,
+ total: array.length,
+ page,
+ page_size,
+ };
+};
+
+// Mock chat history data
+const mockChatHistory = [
+ {
+ id: 'chat-001',
+ title: '关于React组件开发的问题',
+ knowledge_base_id: 'kb-001',
+ knowledge_base_name: 'Frontend Development',
+ message_count: 5,
+ created_at: '2024-03-15T10:30:00Z',
+ updated_at: '2024-03-15T11:45:00Z',
+ },
+ {
+ id: 'chat-002',
+ title: 'Vue.js性能优化讨论',
+ knowledge_base_id: 'kb-001',
+ knowledge_base_name: 'Frontend Development',
+ message_count: 3,
+ created_at: '2024-03-14T15:20:00Z',
+ updated_at: '2024-03-14T16:10:00Z',
+ },
+ {
+ id: 'chat-003',
+ title: '后端API集成问题',
+ knowledge_base_id: 'kb-002',
+ knowledge_base_name: 'Backend Technologies',
+ message_count: 4,
+ created_at: '2024-03-13T09:15:00Z',
+ updated_at: '2024-03-13T10:30:00Z',
+ },
+];
+
+// Mock chat history functions
+const mockGetChatHistory = (params) => {
+ const { page = 1, page_size = 10 } = params;
+ return paginate(mockChatHistory, page_size, page);
+};
+
+const mockCreateChat = (data) => {
+ const newChat = {
+ id: `chat-${uuidv4().slice(0, 8)}`,
+ title: data.title || '新对话',
+ knowledge_base_id: data.knowledge_base_id,
+ knowledge_base_name: data.knowledge_base_name,
+ message_count: 0,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ };
+ mockChatHistory.unshift(newChat);
+ return newChat;
+};
+
+const mockUpdateChat = (id, data) => {
+ const index = mockChatHistory.findIndex((chat) => chat.id === id);
+ if (index === -1) {
+ throw new Error('Chat not found');
+ }
+ const updatedChat = {
+ ...mockChatHistory[index],
+ ...data,
+ updated_at: new Date().toISOString(),
+ };
+ mockChatHistory[index] = updatedChat;
+ return updatedChat;
+};
+
+const mockDeleteChat = (id) => {
+ const index = mockChatHistory.findIndex((chat) => chat.id === id);
+ if (index === -1) {
+ throw new Error('Chat not found');
+ }
+ mockChatHistory.splice(index, 1);
+ return { success: true };
+};
+
+// 模拟聊天消息数据
+const chatMessages = {};
+
+// 权限申请列表的 mock 数据
+const mockPendingRequests = [
+ {
+ id: 1,
+ knowledge_base: 'f13c4bdb-eb03-4ce2-b83c-30917351fb72',
+ applicant: 'f2799611-7a3d-436d-b3fa-3789bdd877e2',
+ permissions: {
+ can_edit: false,
+ can_read: true,
+ can_delete: false,
+ },
+ status: 'pending',
+ reason: '需要访问知识库进行学习',
+ response_message: null,
+ expires_at: '2025-03-19T00:17:43.781000Z',
+ created_at: '2025-03-12T00:17:44.044351Z',
+ updated_at: '2025-03-12T00:17:44.044369Z',
+ },
+ {
+ id: 2,
+ knowledge_base: 'f13c4bdb-eb03-4ce2-b83c-30917351fb73',
+ applicant: 'f2799611-7a3d-436d-b3fa-3789bdd877e3',
+ permissions: {
+ can_edit: true,
+ can_read: true,
+ can_delete: false,
+ },
+ status: 'pending',
+ reason: '需要编辑和更新文档',
+ response_message: null,
+ expires_at: '2025-03-20T00:17:43.781000Z',
+ created_at: '2025-03-12T00:17:44.044351Z',
+ updated_at: '2025-03-12T00:17:44.044369Z',
+ },
+];
+
+// 用户权限列表的 mock 数据
+const mockUserPermissions = [
+ {
+ id: 'perm-001',
+ user: {
+ id: 'user-001',
+ username: 'johndoe',
+ name: 'John Doe',
+ email: 'john@example.com',
+ department: '研发部',
+ group: '前端开发组',
+ },
+ knowledge_base: {
+ id: 'kb-001',
+ name: 'Frontend Development Guide',
+ },
+ permissions: {
+ can_read: true,
+ can_edit: true,
+ can_delete: true,
+ can_manage: true,
+ },
+ granted_at: '2024-01-15T10:00:00Z',
+ granted_by: {
+ id: 'user-admin',
+ username: 'admin',
+ name: 'System Admin',
+ },
+ },
+ {
+ id: 'perm-002',
+ user: {
+ id: 'user-002',
+ username: 'janedoe',
+ name: 'Jane Doe',
+ email: 'jane@example.com',
+ department: '研发部',
+ group: '前端开发组',
+ },
+ knowledge_base: {
+ id: 'kb-001',
+ name: 'Frontend Development Guide',
+ },
+ permissions: {
+ can_read: true,
+ can_edit: true,
+ can_delete: false,
+ can_manage: false,
+ },
+ granted_at: '2024-01-20T14:30:00Z',
+ granted_by: {
+ id: 'user-001',
+ username: 'johndoe',
+ name: 'John Doe',
+ },
+ },
+ {
+ id: 'perm-003',
+ user: {
+ id: 'user-003',
+ username: 'alexsmith',
+ name: 'Alex Smith',
+ email: 'alex@example.com',
+ department: '研发部',
+ group: '后端开发组',
+ },
+ knowledge_base: {
+ id: 'kb-001',
+ name: 'Frontend Development Guide',
+ },
+ permissions: {
+ can_read: true,
+ can_edit: false,
+ can_delete: false,
+ can_manage: false,
+ },
+ granted_at: '2024-02-01T09:15:00Z',
+ granted_by: {
+ id: 'user-001',
+ username: 'johndoe',
+ name: 'John Doe',
+ },
+ },
+];
+
+// Mock API handlers for permissions
+const mockPermissionApi = {
+ // 获取待处理的权限申请列表
+ getPendingRequests: () => {
+ return {
+ code: 200,
+ message: 'success',
+ data: {
+ items: mockPendingRequests,
+ total: mockPendingRequests.length,
+ },
+ };
+ },
+
+ // 获取用户权限列表
+ getUserPermissions: (knowledgeBaseId) => {
+ const permissions = mockUserPermissions.filter((perm) => perm.knowledge_base.id === knowledgeBaseId);
+ return {
+ code: 200,
+ message: 'success',
+ data: {
+ items: permissions,
+ total: permissions.length,
+ },
+ };
+ },
+
+ // 处理权限申请
+ handlePermissionRequest: (requestId, action) => {
+ const request = mockPendingRequests.find((req) => req.id === requestId);
+ if (!request) {
+ return {
+ code: 404,
+ message: 'Permission request not found',
+ };
+ }
+
+ request.status = action === 'approve' ? 'approved' : 'rejected';
+
+ if (action === 'approve') {
+ // 如果批准,添加新的权限记录
+ const newPermission = {
+ id: `perm-${Date.now()}`,
+ user: request.user,
+ knowledge_base: request.knowledge_base,
+ permissions: {
+ can_read: true,
+ can_edit: request.request_type === 'edit',
+ can_delete: false,
+ can_manage: false,
+ },
+ granted_at: new Date().toISOString(),
+ granted_by: mockCurrentUser,
+ };
+ mockUserPermissions.push(newPermission);
+ }
+
+ return {
+ code: 200,
+ message: 'success',
+ data: request,
+ };
+ },
+
+ // 更新用户权限
+ updateUserPermission: (permissionId, permissions) => {
+ const permission = mockUserPermissions.find((perm) => perm.id === permissionId);
+ if (!permission) {
+ return {
+ code: 404,
+ message: 'Permission not found',
+ };
+ }
+
+ permission.permissions = {
+ ...permission.permissions,
+ ...permissions,
+ };
+
+ return {
+ code: 200,
+ message: 'success',
+ data: permission,
+ };
+ },
+
+ // 删除用户权限
+ deleteUserPermission: (permissionId) => {
+ const index = mockUserPermissions.findIndex((perm) => perm.id === permissionId);
+ if (index === -1) {
+ return {
+ code: 404,
+ message: 'Permission not found',
+ };
+ }
+
+ mockUserPermissions.splice(index, 1);
+
+ return {
+ code: 200,
+ message: 'success',
+ };
+ },
+};
+
+// Mock API functions
+export const mockGet = async (url, config = {}) => {
+ console.log(`[MOCK API] GET ${url}`, config);
+
+ // Simulate network delay
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
+ // Get current user
+ if (url === '/users/me/') {
+ return {
+ user: mockUsers[0], // 默认返回第一个用户
+ };
+ }
+
+ // Get knowledge bases
+ if (url === '/knowledge-bases/') {
+ const params = config.params || { page: 1, page_size: 10 };
+ const result = paginate(knowledgeBases, params.page_size, params.page);
+
+ return {
+ data: {
+ code: 200,
+ message: '获取知识库列表成功',
+ data: {
+ total: result.total,
+ page: result.page,
+ page_size: result.page_size,
+ items: result.items,
+ },
+ },
+ };
+ }
+
+ // Get knowledge base details
+ if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
+ const id = url.split('/')[2];
+ const knowledgeBase = knowledgeBases.find((kb) => kb.id === id);
+
+ if (!knowledgeBase) {
+ throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
+ }
+
+ return {
+ data: {
+ code: 200,
+ message: 'success',
+ data: {
+ knowledge_base: knowledgeBase,
+ },
+ },
+ };
+ }
+
+ // Get chat history
+ if (url === '/chat-history/') {
+ const params = config.params || { page: 1, page_size: 10 };
+ const result = mockGetChatHistory(params);
+ return {
+ data: {
+ code: 200,
+ message: 'success',
+ data: result,
+ },
+ };
+ }
+
+ // Get chat messages
+ if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) {
+ const chatId = url.split('/')[2];
+
+ // 如果没有该聊天的消息记录,创建一个空数组
+ if (!chatMessages[chatId]) {
+ chatMessages[chatId] = [];
+
+ // 添加一条欢迎消息
+ const chat = mockChatHistory.find((chat) => chat.id === chatId);
+ if (chat) {
+ chatMessages[chatId].push({
+ id: uuidv4(),
+ chat_id: chatId,
+ sender: 'bot',
+ content: `欢迎使用 ${chat.knowledge_base_name},有什么可以帮助您的?`,
+ type: 'text',
+ created_at: new Date().toISOString(),
+ });
+ }
+ }
+
+ return {
+ code: 200,
+ message: '获取成功',
+ data: {
+ messages: chatMessages[chatId] || [],
+ },
+ };
+ }
+
+ // Knowledge base search
+ if (url === '/knowledge-bases/search/') {
+ const { keyword = '', page = 1, page_size = 10 } = config.params || {};
+ const filtered = knowledgeBases.filter(
+ (kb) =>
+ kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
+ kb.description.toLowerCase().includes(keyword.toLowerCase()) ||
+ kb.tags.some((tag) => tag.toLowerCase().includes(keyword.toLowerCase()))
+ );
+ const result = paginate(filtered, page_size, page);
+ return {
+ code: 200,
+ message: 'success',
+ data: result,
+ };
+ }
+
+ // 用户权限管理 - 获取用户列表
+ if (url === '/users/permissions/') {
+ return {
+ code: 200,
+ message: 'success',
+ data: {
+ users: mockUsers,
+ },
+ };
+ }
+
+ // 用户权限管理 - 获取待处理申请
+ if (url === '/permissions/pending/') {
+ return {
+ code: 200,
+ message: 'success',
+ data: {
+ items: mockPendingRequests,
+ total: mockPendingRequests.length,
+ },
+ };
+ }
+
+ // 用户权限管理 - 获取用户权限详情
+ if (url.match(/\/users\/(.+)\/permissions\//)) {
+ const userId = url.match(/\/users\/(.+)\/permissions\//)[1];
+
+ return {
+ code: 200,
+ message: 'success',
+ data: {
+ permissions: mockUserPermissions[userId] || [],
+ },
+ };
+ }
+
+ throw { response: { status: 404, data: { message: 'Not found' } } };
+};
+
+export const mockPost = async (url, data) => {
+ console.log(`[MOCK API] POST ${url}`, data);
+
+ // Simulate network delay
+ await new Promise((resolve) => setTimeout(resolve, 800));
+
+ // Login
+ if (url === '/auth/login/') {
+ const { username, password } = data;
+ const user = mockUsers.find((u) => u.username === username && u.password === password);
+
+ if (!user) {
+ throw {
+ response: {
+ status: 401,
+ data: {
+ code: 401,
+ message: '用户名或密码错误',
+ },
+ },
+ };
+ }
+
+ // 在实际应用中,这里应该生成 JWT token
+ const token = `mock-jwt-token-${uuidv4()}`;
+
+ return {
+ code: 200,
+ message: '登录成功',
+ data: {
+ token,
+ id: user.id,
+ username: user.username,
+ email: user.email,
+ name: user.name,
+ department: user.department,
+ group: user.group,
+ role: user.role,
+ avatar: user.avatar,
+ },
+ };
+ }
+
+ // Create knowledge base
+ if (url === '/knowledge-bases/') {
+ const newKnowledgeBase = {
+ id: `kb-${uuidv4().slice(0, 8)}`,
+ name: data.name,
+ description: data.description || '',
+ desc: data.desc || data.description || '',
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ create_time: new Date().toISOString(),
+ update_time: new Date().toISOString(),
+ type: data.type || 'private',
+ department: data.department || null,
+ group: data.group || null,
+ owner: {
+ id: 'user-001',
+ username: 'johndoe',
+ email: 'john@example.com',
+ },
+ document_count: 0,
+ tags: data.tags || [],
+ permissions: {
+ can_edit: true,
+ can_read: true,
+ },
+ documents: [],
+ };
+
+ knowledgeBases.push(newKnowledgeBase);
+
+ return {
+ code: 200,
+ message: '知识库创建成功',
+ data: {
+ knowledge_base: newKnowledgeBase,
+ external_id: uuidv4(),
+ },
+ };
+ }
+
+ // Create new chat
+ if (url === '/chat-history/') {
+ const newChat = mockCreateChat(data);
+ return {
+ code: 200,
+ message: 'success',
+ data: {
+ chat: newChat,
+ },
+ };
+ }
+
+ // Send chat message
+ if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) {
+ const chatId = url.split('/')[2];
+
+ // 如果没有该聊天的消息记录,创建一个空数组
+ if (!chatMessages[chatId]) {
+ chatMessages[chatId] = [];
+ }
+
+ // 创建用户消息
+ const userMessage = {
+ id: uuidv4(),
+ chat_id: chatId,
+ sender: 'user',
+ content: data.content,
+ type: data.type || 'text',
+ created_at: new Date().toISOString(),
+ };
+
+ // 添加用户消息
+ chatMessages[chatId].push(userMessage);
+
+ // 创建机器人回复
+ const botMessage = {
+ id: uuidv4(),
+ chat_id: chatId,
+ sender: 'bot',
+ content: `这是对您问题的回复:${data.content}`,
+ type: 'text',
+ created_at: new Date(Date.now() + 1000).toISOString(),
+ };
+
+ // 添加机器人回复
+ chatMessages[chatId].push(botMessage);
+
+ // 更新聊天的最后一条消息和时间
+ const chatIndex = mockChatHistory.findIndex((chat) => chat.id === chatId);
+ if (chatIndex !== -1) {
+ mockChatHistory[chatIndex].message_count = (mockChatHistory[chatIndex].message_count || 0) + 2;
+ mockChatHistory[chatIndex].updated_at = new Date().toISOString();
+ }
+
+ return {
+ code: 200,
+ message: '发送成功',
+ data: {
+ user_message: userMessage,
+ bot_message: botMessage,
+ },
+ };
+ }
+
+ // 批准权限申请
+ if (url === '/permissions/approve/') {
+ const { id, responseMessage } = data;
+
+ // 从待处理列表中移除该申请
+ mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id);
+
+ return {
+ code: 200,
+ message: 'Permission approved successfully',
+ data: {
+ id: id,
+ status: 'approved',
+ response_message: responseMessage,
+ },
+ };
+ }
+
+ // 拒绝权限申请
+ if (url === '/permissions/reject/') {
+ const { id, responseMessage } = data;
+
+ // 从待处理列表中移除该申请
+ mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id);
+
+ return {
+ code: 200,
+ message: 'Permission rejected successfully',
+ data: {
+ id: id,
+ status: 'rejected',
+ response_message: responseMessage,
+ },
+ };
+ }
+
+ throw { response: { status: 404, data: { message: 'Not found' } } };
+};
+
+export const mockPut = async (url, data) => {
+ console.log(`[MOCK API] PUT ${url}`, data);
+
+ // Simulate network delay
+ await new Promise((resolve) => setTimeout(resolve, 600));
+
+ // Update knowledge base
+ if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
+ const id = url.split('/')[2];
+ const index = knowledgeBases.findIndex((kb) => kb.id === id);
+
+ if (index === -1) {
+ throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
+ }
+
+ const updatedKnowledgeBase = {
+ ...knowledgeBases[index],
+ ...data,
+ updated_at: new Date().toISOString(),
+ update_time: new Date().toISOString(),
+ };
+
+ knowledgeBases[index] = updatedKnowledgeBase;
+
+ // 返回与 mockPost 类似的格式
+ return {
+ code: 200,
+ message: '知识库更新成功',
+ data: {
+ knowledge_base: updatedKnowledgeBase,
+ external_id: knowledgeBases[index].id,
+ },
+ };
+ }
+
+ // Update chat
+ if (url.match(/^\/chat-history\/[^/]+\/$/)) {
+ const id = url.split('/')[2];
+ return { data: mockUpdateChat(id, data) };
+ }
+
+ // 更新用户权限
+ if (url.match(/\/users\/(.+)\/permissions\//)) {
+ const userId = url.match(/\/users\/(.+)\/permissions\//)[1];
+ const { permissions } = data;
+
+ // 将权限更新应用到模拟数据
+ if (mockUserPermissions[userId]) {
+ // 遍历permissions对象,更新对应知识库的权限
+ Object.entries(permissions).forEach(([knowledgeBaseId, permissionType]) => {
+ // 查找该用户的该知识库权限
+ const permissionIndex = mockUserPermissions[userId].findIndex(
+ (p) => p.knowledge_base.id === knowledgeBaseId
+ );
+
+ if (permissionIndex !== -1) {
+ // 根据权限类型设置具体权限
+ const permission = {
+ can_read: permissionType === 'read' || permissionType === 'edit' || permissionType === 'admin',
+ can_edit: permissionType === 'edit' || permissionType === 'admin',
+ can_admin: permissionType === 'admin',
+ };
+
+ // 更新权限
+ mockUserPermissions[userId][permissionIndex].permission = permission;
+ }
+ });
+ }
+
+ return {
+ code: 200,
+ message: 'Permissions updated successfully',
+ data: {
+ permissions: permissions,
+ },
+ };
+ }
+
+ throw { response: { status: 404, data: { message: 'Not found' } } };
+};
+
+export const mockDelete = async (url) => {
+ console.log(`[MOCK API] DELETE ${url}`);
+
+ // Simulate network delay
+ await new Promise((resolve) => setTimeout(resolve, 800));
+
+ // Delete knowledge base
+ if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
+ const id = url.split('/')[2];
+ const index = knowledgeBases.findIndex((kb) => kb.id === id);
+
+ if (index === -1) {
+ throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
+ }
+
+ knowledgeBases.splice(index, 1);
+ return { success: true };
+ }
+
+ // Delete chat
+ if (url.match(/^\/chat-history\/[^/]+\/$/)) {
+ const id = url.split('/')[2];
+ return { data: mockDeleteChat(id) };
+ }
+
+ throw { response: { status: 404, data: { message: 'Not found' } } };
+};
+
+// Reset mock data to initial state (useful for testing)
+export const resetMockData = () => {
+ knowledgeBases = [...mockKnowledgeBases];
+};
+
+// 添加权限相关的 API 处理
+export const mockApi = {
+ // ... existing api handlers ...
+
+ // 权限管理相关的 API
+ 'GET /api/permissions/pending': () => mockPermissionApi.getPendingRequests(),
+ 'GET /api/permissions/users/:knowledgeBaseId': (params) =>
+ mockPermissionApi.getUserPermissions(params.knowledgeBaseId),
+ 'POST /api/permissions/handle/:requestId': (params, body) =>
+ mockPermissionApi.handlePermissionRequest(params.requestId, body.action),
+ 'PUT /api/permissions/:permissionId': (params, body) =>
+ mockPermissionApi.updateUserPermission(params.permissionId, body.permissions),
+ 'DELETE /api/permissions/:permissionId': (params) => mockPermissionApi.deleteUserPermission(params.permissionId),
+};
diff --git a/src/services/permissionService.js b/src/services/permissionService.js
new file mode 100644
index 0000000..a77532e
--- /dev/null
+++ b/src/services/permissionService.js
@@ -0,0 +1,56 @@
+import { post } from './api';
+
+/**
+ * 计算权限过期时间
+ * @param {string} duration - 权限持续时间,如 '一周', '一个月', '三个月', '六个月', '永久'
+ * @returns {string} - ISO 格式的日期字符串
+ */
+export const calculateExpiresAt = (duration) => {
+ const now = new Date();
+ switch (duration) {
+ case '一周':
+ now.setDate(now.getDate() + 7);
+ break;
+ case '一个月':
+ now.setMonth(now.getMonth() + 1);
+ break;
+ case '三个月':
+ now.setMonth(now.getMonth() + 3);
+ break;
+ case '六个月':
+ now.setMonth(now.getMonth() + 6);
+ break;
+ case '永久':
+ // 设置为较远的未来日期
+ now.setFullYear(now.getFullYear() + 10);
+ break;
+ default:
+ now.setDate(now.getDate() + 7);
+ }
+ return now.toISOString();
+};
+
+/**
+ * 申请知识库访问权限(已废弃,请使用store/knowledgeBase/knowledgeBase.thunks中的requestKnowledgeBaseAccess)
+ * @deprecated 请使用Redux thunk版本
+ * @param {Object} requestData - 请求数据
+ * @param {string} requestData.id - 知识库ID
+ * @param {string} requestData.accessType - 访问类型,如 '只读访问', '编辑权限'
+ * @param {string} requestData.duration - 访问时长,如 '一周', '一个月'
+ * @param {string} requestData.reason - 申请原因
+ * @returns {Promise} - API 请求的 Promise
+ */
+export const legacyRequestKnowledgeBaseAccess = async (requestData) => {
+ const apiRequestData = {
+ knowledge_base: requestData.id,
+ permissions: {
+ can_read: true,
+ can_edit: requestData.accessType === '编辑权限',
+ can_delete: false,
+ },
+ reason: requestData.reason,
+ expires_at: calculateExpiresAt(requestData.duration),
+ };
+
+ return post('/permissions/', apiRequestData);
+};
diff --git a/src/services/websocket.js b/src/services/websocket.js
new file mode 100644
index 0000000..7bfad05
--- /dev/null
+++ b/src/services/websocket.js
@@ -0,0 +1,234 @@
+import { addNotification, markNotificationAsRead } from '../store/notificationCenter/notificationCenter.slice';
+import store from '../store/store'; // 修改为默认导出
+
+// 从环境变量获取 API URL
+const API_URL = import.meta.env.VITE_API_URL || '';
+// 将 HTTP URL 转换为 WebSocket URL
+const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, '');
+
+let socket = null;
+let reconnectTimer = null;
+let pingInterval = null;
+const RECONNECT_DELAY = 5000; // 5秒后尝试重连
+const PING_INTERVAL = 30000; // 30秒发送一次ping
+
+/**
+ * 初始化WebSocket连接
+ * @returns {Promise} WebSocket连接实例
+ */
+export const initWebSocket = () => {
+ return new Promise((resolve, reject) => {
+ // 如果已经有一个连接,先关闭它
+ if (socket && socket.readyState !== WebSocket.CLOSED) {
+ socket.close();
+ }
+
+ // 清除之前的定时器
+ if (reconnectTimer) clearTimeout(reconnectTimer);
+ if (pingInterval) clearInterval(pingInterval);
+
+ try {
+ // 从sessionStorage获取token
+ const encryptedToken = sessionStorage.getItem('token');
+ if (!encryptedToken) {
+ console.error('No token found, cannot connect to notification service');
+ reject(new Error('No token found'));
+ return;
+ }
+
+ const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${encryptedToken}`;
+ socket = new WebSocket(wsUrl);
+
+ // 连接建立时的处理
+ socket.onopen = () => {
+ console.log('WebSocket connection established');
+
+ // 订阅通知频道
+ subscribeToNotifications();
+
+ // 设置定时发送ping消息
+ pingInterval = setInterval(() => {
+ if (socket.readyState === WebSocket.OPEN) {
+ sendPing();
+ }
+ }, PING_INTERVAL);
+
+ resolve(socket);
+ };
+
+ // 接收消息的处理
+ socket.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ handleWebSocketMessage(data);
+ } catch (error) {
+ console.error('Error parsing WebSocket message:', error);
+ }
+ };
+
+ // 错误处理
+ socket.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ reject(error);
+ };
+
+ // 连接关闭时的处理
+ socket.onclose = (event) => {
+ console.log(`WebSocket connection closed: ${event.code} ${event.reason}`);
+
+ // 清除ping定时器
+ if (pingInterval) clearInterval(pingInterval);
+
+ // 如果不是正常关闭,尝试重连
+ if (event.code !== 1000) {
+ reconnectTimer = setTimeout(() => {
+ console.log('Attempting to reconnect WebSocket...');
+ initWebSocket().catch((err) => {
+ console.error('Failed to reconnect WebSocket:', err);
+ });
+ }, RECONNECT_DELAY);
+ }
+ };
+ } catch (error) {
+ console.error('Error initializing WebSocket:', error);
+ reject(error);
+ }
+ });
+};
+
+/**
+ * 订阅通知频道
+ */
+export const subscribeToNotifications = () => {
+ if (socket && socket.readyState === WebSocket.OPEN) {
+ const subscribeMessage = {
+ type: 'subscribe',
+ channel: 'notifications',
+ };
+ socket.send(JSON.stringify(subscribeMessage));
+ }
+};
+
+/**
+ * 发送ping消息(保持连接活跃)
+ */
+export const sendPing = () => {
+ if (socket && socket.readyState === WebSocket.OPEN) {
+ const pingMessage = {
+ type: 'ping',
+ };
+ socket.send(JSON.stringify(pingMessage));
+ }
+};
+
+/**
+ * 确认已读通知
+ * @param {string} notificationId 通知ID
+ */
+export const acknowledgeNotification = (notificationId) => {
+ if (socket && socket.readyState === WebSocket.OPEN) {
+ const ackMessage = {
+ type: 'acknowledge',
+ notification_id: notificationId,
+ };
+ socket.send(JSON.stringify(ackMessage));
+
+ // 使用 store.dispatch 替代 dispatch
+ store.dispatch(markNotificationAsRead(notificationId));
+ }
+};
+
+/**
+ * 关闭WebSocket连接
+ */
+export const closeWebSocket = () => {
+ if (socket) {
+ socket.close(1000, 'Normal closure');
+ socket = null;
+ }
+
+ if (reconnectTimer) {
+ clearTimeout(reconnectTimer);
+ reconnectTimer = null;
+ }
+
+ if (pingInterval) {
+ clearInterval(pingInterval);
+ pingInterval = null;
+ }
+};
+
+/**
+ * 处理接收到的WebSocket消息
+ * @param {Object} data 解析后的消息数据
+ */
+const handleWebSocketMessage = (data) => {
+ switch (data.type) {
+ case 'connection_established':
+ console.log(`Connection established for user: ${data.user_id}`);
+ break;
+
+ case 'notification':
+ console.log('Received notification:', data);
+ // 将通知添加到Redux store
+ store.dispatch(addNotification(processNotification(data)));
+ break;
+
+ case 'pong':
+ console.log(`Received pong at ${data.timestamp}`);
+ break;
+
+ case 'error':
+ console.error(`WebSocket error: ${data.code} - ${data.message}`);
+ break;
+
+ default:
+ console.log('Received unknown message type:', data);
+ }
+};
+
+/**
+ * 处理通知数据,转换为应用内通知格式
+ * @param {Object} data 通知数据
+ * @returns {Object} 处理后的通知数据
+ */
+const processNotification = (data) => {
+ const { data: notificationData } = data;
+
+ let icon = 'bi-info-circle';
+ if (notificationData.category === 'system') {
+ icon = 'bi-info-circle';
+ } else if (notificationData.category === 'permission') {
+ icon = 'bi-shield';
+ }
+
+ // 计算时间显示
+ const createdAt = new Date(notificationData.created_at);
+ const now = new Date();
+ const diffMs = now - createdAt;
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMins / 60);
+ const diffDays = Math.floor(diffHours / 24);
+
+ let timeDisplay;
+ if (diffMins < 60) {
+ timeDisplay = `${diffMins}分钟前`;
+ } else if (diffHours < 24) {
+ timeDisplay = `${diffHours}小时前`;
+ } else {
+ timeDisplay = `${diffDays}天前`;
+ }
+
+ return {
+ id: notificationData.id,
+ type: notificationData.category,
+ icon,
+ title: notificationData.title,
+ content: notificationData.content,
+ time: timeDisplay,
+ hasDetail: true,
+ isRead: notificationData.is_read,
+ created_at: notificationData.created_at,
+ metadata: notificationData.metadata || {},
+ };
+};
diff --git a/src/store/auth/auth.slice.js b/src/store/auth/auth.slice.js
new file mode 100644
index 0000000..e5f2bb4
--- /dev/null
+++ b/src/store/auth/auth.slice.js
@@ -0,0 +1,63 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
+
+const setPending = (state) => {
+ state.loading = true;
+ state.error = null;
+ state.user = null;
+};
+
+const setFulfilled = (state, action) => {
+ state.user = action.payload;
+ state.loading = false;
+ state.error = null;
+};
+
+const setRejected = (state, action) => {
+ state.error = action.payload;
+ state.loading = false;
+};
+
+const authSlice = createSlice({
+ name: 'auth',
+ initialState: {
+ loading: false,
+ error: null,
+ user: null,
+ },
+ reducers: {
+ login: (state, action) => {
+ state.user = action.payload;
+ },
+ logout: (state) => {
+ state.user = null;
+ state.error = null;
+ state.loading = false;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(checkAuthThunk.pending, setPending)
+ .addCase(checkAuthThunk.fulfilled, setFulfilled)
+ .addCase(checkAuthThunk.rejected, setRejected)
+
+ .addCase(loginThunk.pending, setPending)
+ .addCase(loginThunk.fulfilled, setFulfilled)
+ .addCase(loginThunk.rejected, setRejected)
+
+ .addCase(signupThunk.pending, setPending)
+ .addCase(signupThunk.fulfilled, setFulfilled)
+ .addCase(signupThunk.rejected, setRejected)
+
+ .addCase(logoutThunk.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(logoutThunk.fulfilled)
+ .addCase(logoutThunk.rejected, setRejected);
+ },
+});
+
+export const { login, logout } = authSlice.actions;
+const authReducer = authSlice.reducer;
+export default authReducer;
diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js
new file mode 100644
index 0000000..3c7b20a
--- /dev/null
+++ b/src/store/auth/auth.thunk.js
@@ -0,0 +1,111 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { get, post } from '../../services/api';
+import { showNotification } from '../notification.slice';
+import { logout } from './auth.slice';
+import CryptoJS from 'crypto-js';
+
+const secretKey = import.meta.env.VITE_SECRETKEY;
+
+export const loginThunk = createAsyncThunk(
+ 'auth/login',
+ async ({ username, password }, { rejectWithValue, dispatch }) => {
+ try {
+ const { message, data, code } = await post('/auth/login/', { username, password });
+ console.log('code', code);
+
+ if (code !== 200) {
+ throw new Error(message || 'Something went wrong');
+ }
+ if (!data) {
+ throw new Error(message || 'Something went wrong');
+ }
+ const { token } = data;
+ // encrypt token
+ const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
+ sessionStorage.setItem('token', encryptedToken);
+
+ return data;
+ } catch (error) {
+ const errorMessage = error.response?.data?.message || 'Something went wrong';
+ console.log(errorMessage);
+ dispatch(
+ showNotification({
+ message: errorMessage,
+ type: 'danger',
+ })
+ );
+ return rejectWithValue(errorMessage);
+ }
+ }
+);
+
+export const signupThunk = createAsyncThunk('auth/signup', async (userData, { rejectWithValue, dispatch }) => {
+ try {
+ // 使用新的注册 API
+ const response = await post('/auth/register/', userData);
+ console.log('注册返回数据:', response);
+
+ // 处理新的返回格式
+ if (response && response.code === 200) {
+ // // 将 token 加密存储到 sessionStorage
+ // const { token } = response.data;
+ // if (token) {
+ // const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
+ // sessionStorage.setItem('token', encryptedToken);
+ // }
+
+ // 显示注册成功通知
+ dispatch(
+ showNotification({
+ message: '注册成功',
+ type: 'success',
+ })
+ );
+ return response.data;
+ }
+
+ return rejectWithValue(response.message || '注册失败');
+ } catch (error) {
+ const errorMessage = error.response?.data?.message || '注册失败,请稍后重试';
+ dispatch(
+ showNotification({
+ message: errorMessage,
+ type: 'danger',
+ })
+ );
+ return rejectWithValue(errorMessage);
+ }
+});
+
+export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
+ try {
+ const { data, message } = await post('/auth/verify-token/');
+ const { user } = data;
+ if (!user) {
+ dispatch(logout());
+ throw new Error(message || 'No token found');
+ }
+ return user;
+ } catch (error) {
+ dispatch(logout());
+ return rejectWithValue(error.response?.data || 'Token verification failed');
+ }
+});
+
+// Async thunk for logging out
+export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => {
+ try {
+ // Send the logout request to the server (this assumes your server clears any session-related info)
+ await post('/auth/logout/');
+ dispatch(logout());
+ } catch (error) {
+ const errorMessage = error.response?.data?.message || 'Log out failed';
+ dispatch(
+ showNotification({
+ message: errorMessage,
+ type: 'danger',
+ })
+ );
+ return rejectWithValue(errorMessage);
+ }
+});
diff --git a/src/store/chat/chat.messages.thunks.js b/src/store/chat/chat.messages.thunks.js
new file mode 100644
index 0000000..e59e461
--- /dev/null
+++ b/src/store/chat/chat.messages.thunks.js
@@ -0,0 +1,45 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { get, post } from '../../services/api';
+
+/**
+ * 获取聊天消息
+ * @param {string} chatId - 聊天ID
+ */
+export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (chatId, { rejectWithValue }) => {
+ try {
+ const response = await get(`/chat-history/${chatId}/messages/`);
+
+ // 处理返回格式
+ if (response && response.code === 200) {
+ return response.data.messages;
+ }
+
+ return response.data?.messages || [];
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to fetch messages');
+ }
+});
+
+/**
+ * 发送聊天消息
+ * @param {Object} params - 消息参数
+ * @param {string} params.chatId - 聊天ID
+ * @param {string} params.content - 消息内容
+ */
+export const sendMessage = createAsyncThunk('chat/sendMessage', async ({ chatId, content }, { rejectWithValue }) => {
+ try {
+ const response = await post(`/chat-history/${chatId}/messages/`, {
+ content,
+ type: 'text',
+ });
+
+ // 处理返回格式
+ if (response && response.code === 200) {
+ return response.data;
+ }
+
+ return response.data || {};
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to send message');
+ }
+});
diff --git a/src/store/chat/chat.slice.js b/src/store/chat/chat.slice.js
new file mode 100644
index 0000000..f74cd04
--- /dev/null
+++ b/src/store/chat/chat.slice.js
@@ -0,0 +1,340 @@
+import { createSlice } from '@reduxjs/toolkit';
+import {
+ fetchAvailableDatasets,
+ fetchChats,
+ createChat,
+ updateChat,
+ deleteChat,
+ createChatRecord,
+ fetchConversationDetail,
+} from './chat.thunks';
+import { fetchMessages, sendMessage } from './chat.messages.thunks';
+
+// 初始状态
+const initialState = {
+ // Chat history state
+ history: {
+ items: [],
+ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
+ error: null,
+ },
+ // Chat session creation state
+ createSession: {
+ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
+ error: null,
+ sessionId: null,
+ },
+ // Chat messages state
+ messages: {
+ items: [],
+ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
+ error: null,
+ },
+ // Send message state
+ sendMessage: {
+ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
+ error: null,
+ },
+ // 可用于聊天的知识库列表
+ availableDatasets: {
+ items: [],
+ status: 'idle',
+ error: null,
+ },
+ // 操作状态(创建、更新、删除)
+ operations: {
+ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
+ error: null,
+ },
+ // 兼容旧版本的state结构
+ list: {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 10,
+ status: 'idle',
+ error: null,
+ },
+ // 当前聊天
+ currentChat: {
+ data: null,
+ status: 'idle',
+ error: null,
+ },
+};
+
+// 创建 slice
+const chatSlice = createSlice({
+ name: 'chat',
+ initialState,
+ reducers: {
+ // 重置操作状态
+ resetOperationStatus: (state) => {
+ state.operations.status = 'idle';
+ state.operations.error = null;
+ },
+
+ // 重置当前聊天
+ resetCurrentChat: (state) => {
+ state.currentChat.data = null;
+ state.currentChat.status = 'idle';
+ state.currentChat.error = null;
+ },
+
+ // 设置当前聊天
+ setCurrentChat: (state, action) => {
+ state.currentChat.data = action.payload;
+ state.currentChat.status = 'succeeded';
+ },
+
+ // 重置消息状态
+ resetMessages: (state) => {
+ state.messages.items = [];
+ state.messages.status = 'idle';
+ state.messages.error = null;
+ },
+
+ // 重置发送消息状态
+ resetSendMessageStatus: (state) => {
+ state.sendMessage.status = 'idle';
+ state.sendMessage.error = null;
+ },
+
+ // 添加消息
+ addMessage: (state, action) => {
+ state.messages.items.push(action.payload);
+ },
+ },
+ extraReducers: (builder) => {
+ // 获取聊天列表
+ builder
+ .addCase(fetchChats.pending, (state) => {
+ state.list.status = 'loading';
+ state.history.status = 'loading';
+ })
+ .addCase(fetchChats.fulfilled, (state, action) => {
+ state.list.status = 'succeeded';
+ state.list.items = action.payload.results;
+ state.list.total = action.payload.total;
+ state.list.page = action.payload.page;
+ state.list.page_size = action.payload.page_size;
+
+ // 同时更新新的状态结构
+ state.history.status = 'succeeded';
+ state.history.items = action.payload.results;
+ state.history.error = null;
+ })
+ .addCase(fetchChats.rejected, (state, action) => {
+ state.list.status = 'failed';
+ state.list.error = action.payload || action.error.message;
+
+ // 同时更新新的状态结构
+ state.history.status = 'failed';
+ state.history.error = action.payload || action.error.message;
+ })
+
+ // 创建聊天
+ .addCase(createChat.pending, (state) => {
+ state.operations.status = 'loading';
+ })
+ .addCase(createChat.fulfilled, (state, action) => {
+ state.operations.status = 'succeeded';
+ state.list.items.unshift(action.payload);
+ state.list.total += 1;
+ state.currentChat.data = action.payload;
+ state.currentChat.status = 'succeeded';
+ })
+ .addCase(createChat.rejected, (state, action) => {
+ state.operations.status = 'failed';
+ state.operations.error = action.payload || action.error.message;
+ })
+
+ // 删除聊天
+ .addCase(deleteChat.pending, (state) => {
+ state.operations.status = 'loading';
+ })
+ .addCase(deleteChat.fulfilled, (state, action) => {
+ state.operations.status = 'succeeded';
+ // 更新旧的状态结构
+ state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
+ // 更新新的状态结构
+ state.history.items = state.history.items.filter((chat) => chat.conversation_id !== action.payload);
+
+ if (state.list.total > 0) {
+ state.list.total -= 1;
+ }
+
+ if (state.currentChat.data && state.currentChat.data.id === action.payload) {
+ state.currentChat.data = null;
+ }
+ })
+ .addCase(deleteChat.rejected, (state, action) => {
+ state.operations.status = 'failed';
+ state.operations.error = action.payload || action.error.message;
+ })
+
+ // 更新聊天
+ .addCase(updateChat.pending, (state) => {
+ state.operations.status = 'loading';
+ })
+ .addCase(updateChat.fulfilled, (state, action) => {
+ state.operations.status = 'succeeded';
+ const index = state.list.items.findIndex((chat) => chat.id === action.payload.id);
+ if (index !== -1) {
+ state.list.items[index] = action.payload;
+ }
+ if (state.currentChat.data && state.currentChat.data.id === action.payload.id) {
+ state.currentChat.data = action.payload;
+ }
+ })
+ .addCase(updateChat.rejected, (state, action) => {
+ state.operations.status = 'failed';
+ state.operations.error = action.payload || action.error.message;
+ })
+
+ // 获取聊天消息
+ .addCase(fetchMessages.pending, (state) => {
+ state.messages.status = 'loading';
+ state.messages.error = null;
+ })
+ .addCase(fetchMessages.fulfilled, (state, action) => {
+ state.messages.status = 'succeeded';
+ state.messages.items = action.payload;
+ })
+ .addCase(fetchMessages.rejected, (state, action) => {
+ state.messages.status = 'failed';
+ state.messages.error = action.error.message;
+ })
+
+ // 发送聊天消息
+ .addCase(sendMessage.pending, (state) => {
+ state.sendMessage.status = 'loading';
+ state.sendMessage.error = null;
+ })
+ .addCase(sendMessage.fulfilled, (state, action) => {
+ state.sendMessage.status = 'succeeded';
+ // 更新消息列表
+ const index = state.messages.items.findIndex(
+ (msg) => msg.content === action.payload.content && msg.sender === action.payload.sender
+ );
+ if (index === -1) {
+ state.messages.items.push(action.payload);
+ }
+ })
+ .addCase(sendMessage.rejected, (state, action) => {
+ state.sendMessage.status = 'failed';
+ state.sendMessage.error = action.error.message;
+ })
+
+ // 处理创建聊天记录
+ .addCase(createChatRecord.pending, (state) => {
+ state.sendMessage.status = 'loading';
+ state.sendMessage.error = null;
+ })
+ .addCase(createChatRecord.fulfilled, (state, action) => {
+ state.sendMessage.status = 'succeeded';
+ // 添加新的消息
+ state.messages.items.push({
+ id: action.payload.id,
+ role: 'user',
+ content: action.meta.arg.question,
+ created_at: new Date().toISOString(),
+ });
+
+ // 添加助手回复
+ if (action.payload.role === 'assistant' && action.payload.content) {
+ state.messages.items.push({
+ id: action.payload.id,
+ role: 'assistant',
+ content: action.payload.content,
+ created_at: action.payload.created_at,
+ });
+ }
+
+ // 更新聊天记录列表
+ const chatExists = state.history.items.some(
+ (chat) => chat.conversation_id === action.payload.conversation_id
+ );
+
+ if (!chatExists) {
+ const newChat = {
+ conversation_id: action.payload.conversation_id,
+ last_message: action.payload.content,
+ last_time: action.payload.created_at,
+ datasets: [
+ {
+ id: action.payload.dataset_id,
+ name: action.payload.dataset_name,
+ },
+ ],
+ dataset_id_list: action.payload.dataset_id_list,
+ message_count: 2, // 用户问题和助手回复
+ };
+
+ state.history.items.unshift(newChat);
+ } else {
+ // 更新已存在聊天的最后消息和时间
+ const chatIndex = state.history.items.findIndex(
+ (chat) => chat.conversation_id === action.payload.conversation_id
+ );
+
+ if (chatIndex !== -1) {
+ state.history.items[chatIndex].last_message = action.payload.content;
+ state.history.items[chatIndex].last_time = action.payload.created_at;
+ state.history.items[chatIndex].message_count += 2; // 新增用户问题和助手回复
+ }
+ }
+ })
+ .addCase(createChatRecord.rejected, (state, action) => {
+ state.sendMessage.status = 'failed';
+ state.sendMessage.error = action.payload || '创建聊天记录失败';
+ })
+
+ // 处理获取可用知识库
+ .addCase(fetchAvailableDatasets.pending, (state) => {
+ state.availableDatasets.status = 'loading';
+ state.availableDatasets.error = null;
+ })
+ .addCase(fetchAvailableDatasets.fulfilled, (state, action) => {
+ state.availableDatasets.status = 'succeeded';
+ state.availableDatasets.items = action.payload || [];
+ state.availableDatasets.error = null;
+ })
+ .addCase(fetchAvailableDatasets.rejected, (state, action) => {
+ state.availableDatasets.status = 'failed';
+ state.availableDatasets.error = action.payload || '获取可用知识库失败';
+ })
+
+ // 获取会话详情
+ .addCase(fetchConversationDetail.pending, (state) => {
+ state.currentChat.status = 'loading';
+ state.currentChat.error = null;
+ })
+ .addCase(fetchConversationDetail.fulfilled, (state, action) => {
+ if (action.payload) {
+ state.currentChat.status = 'succeeded';
+ state.currentChat.data = action.payload;
+ } else {
+ state.currentChat.status = 'idle';
+ state.currentChat.data = null;
+ }
+ })
+ .addCase(fetchConversationDetail.rejected, (state, action) => {
+ state.currentChat.status = 'failed';
+ state.currentChat.error = action.payload || action.error.message;
+ });
+ },
+});
+
+// 导出 actions
+export const {
+ resetOperationStatus,
+ resetCurrentChat,
+ setCurrentChat,
+ resetMessages,
+ resetSendMessageStatus,
+ addMessage,
+} = chatSlice.actions;
+
+// 导出 reducer
+export default chatSlice.reducer;
diff --git a/src/store/chat/chat.thunks.js b/src/store/chat/chat.thunks.js
new file mode 100644
index 0000000..3a29ba8
--- /dev/null
+++ b/src/store/chat/chat.thunks.js
@@ -0,0 +1,177 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { get, post, put, del } from '../../services/api';
+import { showNotification } from '../notification.slice';
+
+/**
+ * 获取聊天列表
+ * @param {Object} params - 查询参数
+ * @param {number} params.page - 页码
+ * @param {number} params.page_size - 每页数量
+ */
+export const fetchChats = createAsyncThunk('chat/fetchChats', async (params = {}, { rejectWithValue }) => {
+ try {
+ const response = await get('/chat-history/', { params });
+
+ // 处理返回格式
+ if (response && response.code === 200) {
+ return {
+ results: response.data.results,
+ total: response.data.total,
+ page: response.data.page || 1,
+ page_size: response.data.page_size || 10,
+ };
+ }
+
+ return { results: [], total: 0, page: 1, page_size: 10 };
+ } catch (error) {
+ console.error('Error fetching chats:', error);
+ return rejectWithValue(error.response?.data?.message || 'Failed to fetch chats');
+ }
+});
+
+/**
+ * 创建新聊天
+ * @param {Object} chatData - 聊天数据
+ * @param {string} chatData.knowledge_base_id - 知识库ID
+ * @param {string} chatData.title - 聊天标题
+ */
+export const createChat = createAsyncThunk('chat/createChat', async (chatData, { rejectWithValue }) => {
+ try {
+ const response = await post('/chat-history/', chatData);
+
+ // 处理返回格式
+ if (response && response.code === 200) {
+ return response.data.chat;
+ }
+
+ return response.data?.chat || {};
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to create chat');
+ }
+});
+
+/**
+ * 更新聊天
+ * @param {Object} params - 更新参数
+ * @param {string} params.id - 聊天ID
+ * @param {Object} params.data - 更新数据
+ */
+export const updateChat = createAsyncThunk('chat/updateChat', async ({ id, data }, { rejectWithValue }) => {
+ try {
+ const response = await put(`/chat-history/${id}/`, data);
+
+ // 处理返回格式
+ if (response && response.code === 200) {
+ return response.data.chat;
+ }
+
+ return response.data?.chat || {};
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to update chat');
+ }
+});
+
+/**
+ * 删除聊天
+ * @param {string} conversationId - 聊天ID
+ */
+export const deleteChat = createAsyncThunk('chat/deleteChat', async (conversationId, { rejectWithValue }) => {
+ try {
+ const response = await del(`/chat-history/conversation/${conversationId}/`);
+
+ // 处理返回格式
+ if (response && response.code === 200) {
+ return conversationId;
+ }
+
+ return conversationId;
+ } catch (error) {
+ console.error('Error deleting chat:', error);
+ return rejectWithValue(error.response?.data?.message || '删除聊天失败');
+ }
+});
+
+/**
+ * 获取可用于聊天的知识库列表
+ */
+export const fetchAvailableDatasets = createAsyncThunk(
+ 'chat/fetchAvailableDatasets',
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await get('/chat-history/available_datasets/');
+
+ if (response && response.code === 200) {
+ return response.data;
+ }
+
+ return rejectWithValue('获取可用知识库列表失败');
+ } catch (error) {
+ console.error('Error fetching available datasets:', error);
+ return rejectWithValue(error.response?.data?.message || '获取可用知识库列表失败');
+ }
+ }
+);
+
+/**
+ * 创建聊天记录
+ * @param {Object} params - 聊天参数
+ * @param {string[]} params.dataset_id_list - 知识库ID列表
+ * @param {string} params.question - 用户问题
+ * @param {string} params.conversation_id - 会话ID,可选
+ */
+export const createChatRecord = createAsyncThunk('chat/createChatRecord', async (params, { rejectWithValue }) => {
+ try {
+ const response = await post('/chat-history/', {
+ dataset_id_list: params.dataset_id_list,
+ question: params.question,
+ conversation_id: params.conversation_id,
+ });
+
+ // 处理返回格式
+ if (response && response.code === 200) {
+ return response.data;
+ }
+
+ return rejectWithValue(response.message || '创建聊天记录失败');
+ } catch (error) {
+ console.error('Error creating chat record:', error);
+ return rejectWithValue(error.response?.data?.message || '创建聊天记录失败');
+ }
+});
+
+/**
+ * 获取会话详情
+ * @param {string} conversationId - 会话ID
+ */
+export const fetchConversationDetail = createAsyncThunk(
+ 'chat/fetchConversationDetail',
+ async (conversationId, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await get('/chat-history/conversation_detail', {
+ params: { conversation_id: conversationId },
+ });
+
+ if (response && response.code === 200) {
+ // 如果存在消息,更新Redux状态
+ if (response.data.messages) {
+ dispatch({
+ type: 'chat/fetchMessages/fulfilled',
+ payload: response.data.messages,
+ });
+ }
+
+ return response.data;
+ }
+
+ return rejectWithValue('获取会话详情失败');
+ } catch (error) {
+ // 如果是新聊天,API会返回404,此时不返回错误
+ if (error.response && error.response.status === 404) {
+ return null;
+ }
+
+ console.error('Error fetching conversation detail:', error);
+ return rejectWithValue(error.response?.data?.message || '获取会话详情失败');
+ }
+ }
+);
diff --git a/src/store/knowledgeBase/knowledgeBase.slice.js b/src/store/knowledgeBase/knowledgeBase.slice.js
new file mode 100644
index 0000000..9648e19
--- /dev/null
+++ b/src/store/knowledgeBase/knowledgeBase.slice.js
@@ -0,0 +1,184 @@
+import { createSlice } from '@reduxjs/toolkit';
+import {
+ fetchKnowledgeBases,
+ createKnowledgeBase,
+ updateKnowledgeBase,
+ deleteKnowledgeBase,
+ changeKnowledgeBaseType,
+ searchKnowledgeBases,
+ requestKnowledgeBaseAccess,
+ getKnowledgeBaseById,
+} from './knowledgeBase.thunks';
+
+const initialState = {
+ knowledgeBases: [],
+ currentKnowledgeBase: null,
+ searchResults: [],
+ searchLoading: false,
+ loading: false,
+ error: null,
+ pagination: {
+ total: 0,
+ page: 1,
+ page_size: 10,
+ total_pages: 1,
+ },
+ batchPermissions: {},
+ batchLoading: false,
+ editStatus: 'idle',
+ requestAccessStatus: 'idle',
+};
+
+const knowledgeBaseSlice = createSlice({
+ name: 'knowledgeBase',
+ initialState,
+ reducers: {
+ clearCurrentKnowledgeBase: (state) => {
+ state.currentKnowledgeBase = null;
+ },
+ clearSearchResults: (state) => {
+ state.searchResults = [];
+ },
+ clearEditStatus: (state) => {
+ state.editStatus = 'idle';
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ // 获取知识库列表
+ .addCase(fetchKnowledgeBases.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchKnowledgeBases.fulfilled, (state, action) => {
+ state.loading = false;
+ state.knowledgeBases = action.payload.items || [];
+ state.pagination = {
+ total: action.payload.total || 0,
+ page: action.payload.page || 1,
+ page_size: action.payload.page_size || 10,
+ total_pages: action.payload.total_pages || 1,
+ };
+ })
+ .addCase(fetchKnowledgeBases.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'Failed to fetch knowledge bases';
+ })
+
+ // 创建知识库
+ .addCase(createKnowledgeBase.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(createKnowledgeBase.fulfilled, (state, action) => {
+ state.loading = false;
+ state.editStatus = 'successful';
+ // 不需要更新 knowledgeBases,因为创建后会跳转到详情页
+ })
+ .addCase(createKnowledgeBase.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'Failed to create knowledge base';
+ state.editStatus = 'failed';
+ })
+
+ // 更新知识库
+ .addCase(updateKnowledgeBase.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(updateKnowledgeBase.fulfilled, (state, action) => {
+ state.loading = false;
+ state.currentKnowledgeBase = action.payload;
+ state.editStatus = 'successful';
+ })
+ .addCase(updateKnowledgeBase.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'Failed to update knowledge base';
+ state.editStatus = 'failed';
+ })
+
+ // 删除知识库
+ .addCase(deleteKnowledgeBase.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(deleteKnowledgeBase.fulfilled, (state, action) => {
+ state.loading = false;
+ state.knowledgeBases = state.knowledgeBases.filter((kb) => kb.id !== action.meta.arg.knowledgeBaseId);
+ })
+ .addCase(deleteKnowledgeBase.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'Failed to delete knowledge base';
+ })
+
+ // 修改知识库类型
+ .addCase(changeKnowledgeBaseType.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(changeKnowledgeBaseType.fulfilled, (state, action) => {
+ state.loading = false;
+ if (state.currentKnowledgeBase) {
+ state.currentKnowledgeBase = {
+ ...state.currentKnowledgeBase,
+ type: action.payload.type,
+ department: action.payload.department,
+ group: action.payload.group,
+ };
+ }
+ state.editStatus = 'successful';
+ })
+ .addCase(changeKnowledgeBaseType.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'Failed to change knowledge base type';
+ state.editStatus = 'failed';
+ })
+
+ // 搜索知识库
+ .addCase(searchKnowledgeBases.pending, (state) => {
+ state.searchLoading = true;
+ state.error = null;
+ })
+ .addCase(searchKnowledgeBases.fulfilled, (state, action) => {
+ state.searchLoading = false;
+ if (action.payload && action.payload.code === 200) {
+ state.searchResults = action.payload.data.items || [];
+ } else {
+ state.searchResults = action.payload.items || [];
+ }
+ })
+ .addCase(searchKnowledgeBases.rejected, (state, action) => {
+ state.searchLoading = false;
+ state.error = action.payload || 'Failed to search knowledge bases';
+ })
+
+ // 申请知识库访问权限
+ .addCase(requestKnowledgeBaseAccess.pending, (state) => {
+ state.requestAccessStatus = 'loading';
+ })
+ .addCase(requestKnowledgeBaseAccess.fulfilled, (state) => {
+ state.requestAccessStatus = 'successful';
+ })
+ .addCase(requestKnowledgeBaseAccess.rejected, (state) => {
+ state.requestAccessStatus = 'failed';
+ })
+
+ // 获取知识库详情
+ .addCase(getKnowledgeBaseById.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(getKnowledgeBaseById.fulfilled, (state, action) => {
+ state.loading = false;
+ state.currentKnowledgeBase = action.payload.knowledge_base || action.payload;
+ })
+ .addCase(getKnowledgeBaseById.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'Failed to get knowledge base details';
+ });
+ },
+});
+
+export const { clearCurrentKnowledgeBase, clearSearchResults, clearEditStatus } = knowledgeBaseSlice.actions;
+
+export default knowledgeBaseSlice.reducer;
diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js
new file mode 100644
index 0000000..00cb565
--- /dev/null
+++ b/src/store/knowledgeBase/knowledgeBase.thunks.js
@@ -0,0 +1,196 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { get, post, put, del } from '../../services/api';
+import { showNotification } from '../notification.slice';
+
+/**
+ * Fetch knowledge bases with pagination
+ * @param {Object} params - Pagination parameters
+ * @param {number} params.page - Page number (default: 1)
+ * @param {number} params.page_size - Page size (default: 10)
+ */
+export const fetchKnowledgeBases = createAsyncThunk(
+ 'knowledgeBase/fetchKnowledgeBases',
+ async ({ page = 1, page_size = 10 } = {}, { rejectWithValue }) => {
+ try {
+ const response = await get('/knowledge-bases/', { params: { page, page_size } });
+ // 处理新的返回格式
+ if (response.data && response.data.code === 200) {
+ return response.data.data;
+ }
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to fetch knowledge bases');
+ }
+ }
+);
+
+/**
+ * Search knowledge bases
+ * @param {Object} params - Search parameters
+ * @param {string} params.keyword - Search keyword
+ * @param {number} params.page - Page number (default: 1)
+ * @param {number} params.page_size - Page size (default: 10)
+ */
+export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => {
+ try {
+ const { keyword, page = 1, page_size = 10 } = params;
+ const response = await get('/knowledge-bases/search', {
+ params: {
+ keyword,
+ page,
+ page_size,
+ },
+ });
+
+ // 处理新的返回格式
+ if (response.data && response.data.code === 200) {
+ return response.data;
+ }
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(error.response?.data || error.message);
+ }
+});
+
+/**
+ * Create a new knowledge base
+ */
+export const createKnowledgeBase = createAsyncThunk(
+ 'knowledgeBase/createKnowledgeBase',
+ async (knowledgeBaseData, { rejectWithValue }) => {
+ try {
+ const response = await post('/knowledge-bases/', knowledgeBaseData);
+
+ // 处理新的返回格式
+ if (response.data && response.data.code === 200) {
+ return response.data.data.knowledge_base;
+ }
+
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to create knowledge base');
+ }
+ }
+);
+
+/**
+ * Get knowledge base details by ID
+ */
+export const getKnowledgeBaseById = createAsyncThunk(
+ 'knowledgeBase/getKnowledgeBaseById',
+ async (id, { rejectWithValue }) => {
+ try {
+ const response = await get(`/knowledge-bases/${id}/`);
+ // 处理新的返回格式
+ if (response.data && response.data.code === 200) {
+ return response.data.data;
+ }
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to get knowledge base details');
+ }
+ }
+);
+
+/**
+ * Update knowledge base
+ * @param {Object} params - Update parameters
+ * @param {string} params.id - Knowledge base ID
+ * @param {Object} params.data - Update data (name, desc)
+ */
+export const updateKnowledgeBase = createAsyncThunk(
+ 'knowledgeBase/updateKnowledgeBase',
+ async ({ id, data }, { rejectWithValue }) => {
+ try {
+ const response = await put(`/knowledge-bases/${id}/`, data);
+
+ // 处理新的返回格式
+ if (response.data && response.data.code === 200) {
+ return response.data.data.knowledge_base;
+ }
+
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to update knowledge base');
+ }
+ }
+);
+
+/**
+ * Delete knowledge base
+ * @param {string} id - Knowledge base ID
+ */
+export const deleteKnowledgeBase = createAsyncThunk(
+ 'knowledgeBase/deleteKnowledgeBase',
+ async (id, { rejectWithValue }) => {
+ try {
+ await del(`/knowledge-bases/${id}/`);
+ return id;
+ } catch (error) {
+ return rejectWithValue(error.response?.data || 'Failed to delete knowledge base');
+ }
+ }
+);
+
+/**
+ * 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.knowledge_base - 知识库ID
+ * @param {Object} params.permissions - 权限对象
+ * @param {string} params.reason - 申请原因
+ * @param {string} params.expires_at - 过期时间
+ * @returns {Promise} - Promise对象
+ */
+export const requestKnowledgeBaseAccess = createAsyncThunk(
+ 'knowledgeBase/requestAccess',
+ async (params, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await post('/permissions/', params);
+ 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);
+ }
+ }
+);
diff --git a/src/store/notification.slice.js b/src/store/notification.slice.js
index 5e49057..b0658cd 100644
--- a/src/store/notification.slice.js
+++ b/src/store/notification.slice.js
@@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
const notificationSlice = createSlice({
name: 'notification',
- initialState: null,
+ initialState: null, // type(success/primary/warning/danger), message, duration
reducers: {
showNotification: (state, action) => action.payload,
hideNotification: () => null,
diff --git a/src/store/notificationCenter/notificationCenter.slice.js b/src/store/notificationCenter/notificationCenter.slice.js
new file mode 100644
index 0000000..9ccce4b
--- /dev/null
+++ b/src/store/notificationCenter/notificationCenter.slice.js
@@ -0,0 +1,123 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const initialState = {
+ notifications: [
+ {
+ id: 1,
+ type: 'permission',
+ icon: 'bi-shield',
+ title: '新的权限请求',
+ content: '张三请求访问销售数据集',
+ time: '10分钟前',
+ hasDetail: true,
+ isRead: false,
+ },
+ {
+ id: 2,
+ type: 'system',
+ icon: 'bi-info-circle',
+ title: '系统更新通知',
+ content: '系统将在今晚23:00进行例行维护',
+ time: '1小时前',
+ hasDetail: false,
+ isRead: false,
+ },
+ {
+ id: 3,
+ type: 'permission',
+ icon: 'bi-shield',
+ title: '新的权限请求',
+ content: '李四请求访问用户数据集',
+ time: '2小时前',
+ hasDetail: true,
+ isRead: false,
+ },
+ {
+ id: 4,
+ type: 'system',
+ icon: 'bi-exclamation-circle',
+ title: '安全提醒',
+ content: '检测到异常登录行为,请及时查看',
+ time: '3小时前',
+ hasDetail: true,
+ isRead: false,
+ },
+ {
+ id: 5,
+ type: 'permission',
+ icon: 'bi-shield',
+ title: '权限变更通知',
+ content: '管理员修改了您的数据访问权限',
+ time: '1天前',
+ hasDetail: true,
+ isRead: false,
+ },
+ ],
+ unreadCount: 5,
+ isConnected: false,
+};
+
+const notificationCenterSlice = createSlice({
+ name: 'notificationCenter',
+ initialState,
+ reducers: {
+ clearNotifications: (state) => {
+ state.notifications = [];
+ state.unreadCount = 0;
+ },
+
+ addNotification: (state, action) => {
+ // 检查通知是否已存在
+ const exists = state.notifications.some((n) => n.id === action.payload.id);
+ if (!exists) {
+ // 将新通知添加到列表的开头
+ state.notifications.unshift(action.payload);
+ // 如果通知未读,增加未读计数
+ if (!action.payload.isRead) {
+ state.unreadCount += 1;
+ }
+ }
+ },
+
+ markNotificationAsRead: (state, action) => {
+ const notification = state.notifications.find((n) => n.id === action.payload);
+ if (notification && !notification.isRead) {
+ notification.isRead = true;
+ state.unreadCount = Math.max(0, state.unreadCount - 1);
+ }
+ },
+
+ markAllNotificationsAsRead: (state) => {
+ state.notifications.forEach((notification) => {
+ notification.isRead = true;
+ });
+ state.unreadCount = 0;
+ },
+
+ setWebSocketConnected: (state, action) => {
+ state.isConnected = action.payload;
+ },
+
+ removeNotification: (state, action) => {
+ const notificationIndex = state.notifications.findIndex((n) => n.id === action.payload);
+ if (notificationIndex !== -1) {
+ const notification = state.notifications[notificationIndex];
+ if (!notification.isRead) {
+ state.unreadCount = Math.max(0, state.unreadCount - 1);
+ }
+ state.notifications.splice(notificationIndex, 1);
+ }
+ },
+ },
+});
+
+export const {
+ clearNotifications,
+ addNotification,
+ markNotificationAsRead,
+ markAllNotificationsAsRead,
+ setWebSocketConnected,
+ removeNotification,
+} = notificationCenterSlice.actions;
+
+export default notificationCenterSlice.reducer;
diff --git a/src/store/permissions/permissions.slice.js b/src/store/permissions/permissions.slice.js
new file mode 100644
index 0000000..5a2a371
--- /dev/null
+++ b/src/store/permissions/permissions.slice.js
@@ -0,0 +1,159 @@
+import { createSlice } from '@reduxjs/toolkit';
+import {
+ fetchUserPermissions,
+ updateUserPermissions,
+ fetchPermissionsThunk,
+ approvePermissionThunk,
+ rejectPermissionThunk,
+ fetchAllUserPermissions,
+} from './permissions.thunks';
+
+const initialState = {
+ users: {
+ items: [],
+ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
+ error: null,
+ },
+ allUsersPermissions: {
+ results: [],
+ total: 0,
+ page: 1,
+ page_size: 10,
+ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
+ error: null,
+ },
+ operations: {
+ status: 'idle',
+ error: null,
+ },
+ pending: {
+ results: [], // 更改为results
+ total: 0, // 添加total
+ page: 1, // 添加page
+ page_size: 10, // 添加page_size
+ status: 'idle',
+ error: null,
+ },
+ approveReject: {
+ status: 'idle',
+ error: null,
+ currentId: null,
+ },
+};
+
+const permissionsSlice = createSlice({
+ name: 'permissions',
+ initialState,
+ reducers: {
+ resetOperationStatus: (state) => {
+ state.operations.status = 'idle';
+ state.operations.error = null;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ // 获取用户权限列表
+ .addCase(fetchUserPermissions.pending, (state) => {
+ state.users.status = 'loading';
+ state.users.error = null;
+ })
+ .addCase(fetchUserPermissions.fulfilled, (state, action) => {
+ state.users.status = 'succeeded';
+ state.users.items = action.payload;
+ })
+ .addCase(fetchUserPermissions.rejected, (state, action) => {
+ state.users.status = 'failed';
+ state.users.error = action.error.message;
+ })
+
+ // 更新用户权限
+ .addCase(updateUserPermissions.pending, (state) => {
+ state.operations.status = 'loading';
+ state.operations.error = null;
+ })
+ .addCase(updateUserPermissions.fulfilled, (state, action) => {
+ state.operations.status = 'succeeded';
+ // 更新用户列表中的权限信息
+ const index = state.users.items.findIndex((user) => user.id === action.payload.userId);
+ if (index !== -1) {
+ state.users.items[index] = {
+ ...state.users.items[index],
+ permissions: action.payload.permissions,
+ };
+ }
+ })
+ .addCase(updateUserPermissions.rejected, (state, action) => {
+ state.operations.status = 'failed';
+ state.operations.error = action.error.message;
+ })
+
+ // 获取待处理申请列表
+ .addCase(fetchPermissionsThunk.pending, (state) => {
+ state.pending.status = 'loading';
+ state.pending.error = null;
+ })
+ .addCase(fetchPermissionsThunk.fulfilled, (state, action) => {
+ state.pending.status = 'succeeded';
+ state.pending.results = action.payload.results || [];
+ state.pending.total = action.payload.total || 0;
+ state.pending.page = action.payload.page || 1;
+ state.pending.page_size = action.payload.page_size || 10;
+ })
+ .addCase(fetchPermissionsThunk.rejected, (state, action) => {
+ state.pending.status = 'failed';
+ state.pending.error = action.error.message;
+ })
+
+ // 批准/拒绝权限申请
+ .addCase(approvePermissionThunk.pending, (state, action) => {
+ state.approveReject.status = 'loading';
+ state.approveReject.error = null;
+ state.approveReject.currentId = action.meta.arg.id;
+ })
+ .addCase(approvePermissionThunk.fulfilled, (state) => {
+ state.approveReject.status = 'succeeded';
+ state.approveReject.currentId = null;
+ })
+ .addCase(approvePermissionThunk.rejected, (state, action) => {
+ state.approveReject.status = 'failed';
+ state.approveReject.error = action.error.message;
+ state.approveReject.currentId = null;
+ })
+
+ .addCase(rejectPermissionThunk.pending, (state, action) => {
+ state.approveReject.status = 'loading';
+ state.approveReject.error = null;
+ state.approveReject.currentId = action.meta.arg.id;
+ })
+ .addCase(rejectPermissionThunk.fulfilled, (state) => {
+ state.approveReject.status = 'succeeded';
+ state.approveReject.currentId = null;
+ })
+ .addCase(rejectPermissionThunk.rejected, (state, action) => {
+ state.approveReject.status = 'failed';
+ state.approveReject.error = action.error.message;
+ state.approveReject.currentId = null;
+ })
+
+ // 获取所有用户及其权限列表
+ .addCase(fetchAllUserPermissions.pending, (state) => {
+ state.allUsersPermissions.status = 'loading';
+ state.allUsersPermissions.error = null;
+ })
+ .addCase(fetchAllUserPermissions.fulfilled, (state, action) => {
+ state.allUsersPermissions.status = 'succeeded';
+ state.allUsersPermissions.results = action.payload.results || [];
+ state.allUsersPermissions.total = action.payload.total || 0;
+ state.allUsersPermissions.page = action.payload.page || 1;
+ state.allUsersPermissions.page_size = action.payload.page_size || 10;
+ })
+ .addCase(fetchAllUserPermissions.rejected, (state, action) => {
+ state.allUsersPermissions.status = 'failed';
+ state.allUsersPermissions.error = action.payload || action.error.message;
+ });
+ },
+});
+
+export const { resetOperationStatus } = permissionsSlice.actions;
+
+export default permissionsSlice.reducer;
diff --git a/src/store/permissions/permissions.thunks.js b/src/store/permissions/permissions.thunks.js
new file mode 100644
index 0000000..67c4d0d
--- /dev/null
+++ b/src/store/permissions/permissions.thunks.js
@@ -0,0 +1,167 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { get, post, put } from '../../services/api';
+import { showNotification } from '../notification.slice';
+
+// 获取权限申请列表
+export const fetchPermissionsThunk = createAsyncThunk(
+ 'permissions/fetchPermissions',
+ async (params = {}, { rejectWithValue }) => {
+ try {
+ const response = await get('/permissions/', { params });
+
+ if (response && response.code === 200) {
+ return {
+ results: response.data.results || [],
+ total: response.data.total || 0,
+ page: response.data.page || 1,
+ page_size: response.data.page_size || 10,
+ };
+ }
+ return rejectWithValue('获取权限申请列表失败');
+ } catch (error) {
+ console.error('获取权限申请列表失败:', error);
+ return rejectWithValue(error.response?.data?.message || '获取权限申请列表失败');
+ }
+ }
+);
+
+// 批准权限申请
+export const approvePermissionThunk = createAsyncThunk(
+ 'permissions/approvePermission',
+ async ({ id, responseMessage }, { rejectWithValue }) => {
+ try {
+ const response = await post(`/permissions/${id}/approve/`, {
+ response_message: responseMessage || '已批准',
+ });
+ return response;
+ } catch (error) {
+ console.error('批准权限申请失败:', error);
+ return rejectWithValue('批准权限申请失败');
+ }
+ }
+);
+
+// 拒绝权限申请
+export const rejectPermissionThunk = createAsyncThunk(
+ 'permissions/rejectPermission',
+ async ({ id, responseMessage }, { rejectWithValue }) => {
+ try {
+ const response = await post(`/permissions/${id}/reject/`, {
+ response_message: responseMessage || '已拒绝',
+ });
+ return response;
+ } catch (error) {
+ console.error('拒绝权限申请失败:', error);
+ return rejectWithValue('拒绝权限申请失败');
+ }
+ }
+);
+
+// 生成模拟数据
+const generateMockUsers = () => {
+ const users = [];
+ const userNames = [
+ { username: 'zhangsan', name: '张三', department: '达人组', position: '达人对接' },
+ { username: 'lisi', name: '李四', department: '达人组', position: '达人对接' },
+ { username: 'wangwu', name: '王五', department: '达人组', position: '达人对接' },
+ { username: 'zhaoliu', name: '赵六', department: '达人组', position: '达人对接' },
+ { username: 'qianqi', name: '钱七', department: '达人组', position: '达人对接' },
+ { username: 'sunba', name: '孙八', department: '达人组', position: '达人对接' },
+ { username: 'zhoujiu', name: '周九', department: '达人组', position: '达人对接' },
+ { username: 'wushi', name: '吴十', department: '达人组', position: '达人对接' },
+ ];
+
+ for (let i = 1; i <= 20; i++) {
+ const randomUser = userNames[Math.floor(Math.random() * userNames.length)];
+ const hasAdminPermission = Math.random() > 0.8; // 20%的概率有管理员权限
+ const hasEditPermission = Math.random() > 0.5; // 50%的概率有编辑权限
+ const hasReadPermission = hasAdminPermission || hasEditPermission || Math.random() > 0.3; // 如果有管理员或编辑权限,一定有读取权限
+
+ users.push({
+ id: i.toString(),
+ username: randomUser.username,
+ name: randomUser.name,
+ department: randomUser.department,
+ position: randomUser.position,
+ permissions_count: {
+ read: hasReadPermission ? 1 : 0,
+ edit: hasEditPermission ? 1 : 0,
+ admin: hasAdminPermission ? 1 : 0,
+ },
+ });
+ }
+
+ return users;
+};
+
+// 获取用户权限列表
+export const fetchUserPermissions = createAsyncThunk(
+ 'permissions/fetchUserPermissions',
+ async (_, { rejectWithValue }) => {
+ try {
+ // 模拟API延迟
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ // 返回模拟数据
+ return generateMockUsers();
+ } catch (error) {
+ return rejectWithValue(error.message);
+ }
+ }
+);
+
+// 更新用户权限
+export const updateUserPermissions = createAsyncThunk(
+ 'permissions/updateUserPermissions',
+ async ({ userId, permissions }, { dispatch, rejectWithValue }) => {
+ try {
+ const response = await put(`/users/${userId}/permissions/`, { permissions });
+
+ if (response && response.code === 200) {
+ dispatch(
+ showNotification({
+ message: '权限更新成功',
+ type: 'success',
+ })
+ );
+ return {
+ userId,
+ permissions: response.data.permissions,
+ };
+ }
+ return rejectWithValue(response?.message || '更新权限失败');
+ } catch (error) {
+ dispatch(
+ showNotification({
+ message: error.message || '更新权限失败',
+ type: 'danger',
+ })
+ );
+ return rejectWithValue(error.message);
+ }
+ }
+);
+
+// 获取所有用户及其权限列表
+export const fetchAllUserPermissions = createAsyncThunk(
+ 'permissions/fetchAllUserPermissions',
+ async (params = {}, { rejectWithValue }) => {
+ try {
+ const response = await get('/permissions/all_permissions/', { params });
+
+ if (response && response.code === 200) {
+ return {
+ total: response.data.total,
+ page: response.data.page,
+ page_size: response.data.page_size,
+ results: response.data.results,
+ };
+ }
+
+ return rejectWithValue('获取用户权限列表失败');
+ } catch (error) {
+ console.error('获取用户权限列表失败:', error);
+ return rejectWithValue(error.response?.data?.message || '获取用户权限列表失败');
+ }
+ }
+);
diff --git a/src/store/store.js b/src/store/store.js
index 3b4dbfc..8cdecb4 100644
--- a/src/store/store.js
+++ b/src/store/store.js
@@ -2,15 +2,25 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore } from 'redux-persist';
import sessionStorage from 'redux-persist/lib/storage/session';
import notificationReducer from './notification.slice.js';
+import authReducer from './auth/auth.slice.js';
+import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
+import chatReducer from './chat/chat.slice.js';
+import permissionsReducer from './permissions/permissions.slice.js';
+import notificationCenterReducer from './notificationCenter/notificationCenter.slice.js';
const rootRducer = combineReducers({
+ auth: authReducer,
notification: notificationReducer,
+ knowledgeBase: knowledgeBaseReducer,
+ chat: chatReducer,
+ permissions: permissionsReducer,
+ notificationCenter: notificationCenterReducer,
});
const persistConfig = {
key: 'root',
storage: sessionStorage,
- whitelist: [],
+ whitelist: ['auth'],
};
// Persist configuration
diff --git a/src/styles/style.scss b/src/styles/style.scss
index 148177b..aba457a 100644
--- a/src/styles/style.scss
+++ b/src/styles/style.scss
@@ -1,5 +1,8 @@
@import 'bootstrap/scss/bootstrap';
+#root {
+ min-width: 24rem;
+}
.dropdown-toggle {
outline: 0;
}
@@ -9,6 +12,9 @@
}
.knowledge-card {
+ min-width: 20rem;
+ cursor: pointer;
+
.hoverdown:hover .hoverdown-menu{
display: block;
color: red;
@@ -27,6 +33,7 @@
gap: 8px;
border-radius: 4px;
color: $dark;
+
&:hover {
background-color: $gray-100;
}
@@ -34,3 +41,106 @@
}
}
}
+
+.auth-form {
+ input {
+ min-width: 300px !important;
+ }
+}
+
+/* 自定义黑色系开关样式 */
+.dark-switch .form-check-input {
+ border: 1px solid #dee2e6;
+ background-color: #fff; /* 关闭状态背景色 */
+}
+
+/* 关闭状态滑块 */
+.dark-switch .form-check-input:not(:checked) {
+ background-image: url("data:image/svg+xml,");
+}
+
+/* 打开状态 */
+.dark-switch .form-check-input:checked {
+ background-color: #000; /* 打开状态背景色 */
+ border-color: #000;
+}
+
+/* 打开状态滑块 */
+.dark-switch .form-check-input:checked {
+ background-image: url("data:image/svg+xml,");
+}
+
+/* 悬停效果 */
+.dark-switch .form-check-input:hover {
+ filter: brightness(0.9);
+}
+
+/* 禁用状态 */
+.dark-switch .form-check-input:disabled {
+ opacity: 0.5;
+ background-color: #e9ecef;
+}
+
+// 通知中心样式
+.notification-item {
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: $gray-100;
+ }
+}
+
+// 黑色主题的开关按钮
+.form-check-input:checked {
+ background-color: $dark;
+ border-color: $dark;
+}
+
+/* 自定义分页样式 */
+.dark-pagination {
+ margin: 0;
+}
+
+.dark-pagination .page-link {
+ color: #000; /* 默认文字颜色 */
+ background-color: #fff; /* 默认背景 */
+ border: 1px solid #dee2e6; /* 边框颜色 */
+ transition: all 0.3s ease; /* 平滑过渡效果 */
+ }
+
+ /* 激活状态 */
+ .dark-pagination .page-item.active .page-link {
+ background-color: #000 !important;
+ border-color: #000;
+ color: #fff !important;
+ }
+
+ /* 悬停状态 */
+ .dark-pagination .page-link:hover {
+ background-color: #f8f9fa; /* 浅灰背景 */
+ border-color: #adb5bd;
+ }
+
+ /* 禁用状态 */
+ .dark-pagination .page-item.disabled .page-link {
+ color: #6c757d !important;
+ background-color: #e9ecef !important;
+ border-color: #dee2e6;
+ pointer-events: none;
+ opacity: 0.7;
+ }
+
+ /* 自定义下拉框 */
+ .dark-select {
+ border: 1px solid #000 !important;
+ color: #000 !important;
+ }
+
+ .dark-select:focus {
+ box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.25); /* 黑色聚焦阴影 */
+ }
+
+ /* 下拉箭头颜色 */
+ .dark-select {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='%23000' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+ }
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
index 90cdbfd..f2ecfab 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,13 +1,23 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig, loadEnv } from 'vite';
+import react from '@vitejs/plugin-react';
// https://vite.dev/config/
-export default defineConfig({
- plugins: [react()],
- build: {
- outDir: '../dist'
- },
- server: {
- port: 8080
- }
-})
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd());
+
+ return {
+ plugins: [react()],
+ build: {
+ outDir: '../dist',
+ },
+ server: {
+ port: env.VITE_PORT,
+ proxy: {
+ '/api': {
+ target: env.VITE_API_URL || 'http://81.69.223.133:3000',
+ changeOrigin: true,
+ },
+ },
+ },
+ };
+});