refactor(frontend): 重构状态管理使用Zustand替代Context API
feat(auth): 添加账户锁定功能及自动解锁机制 feat(user): 在用户模型中添加lockedUntil字段 feat(api): 实现账户锁定逻辑和剩余尝试次数提示 perf(3d): 优化3D场景状态管理性能 perf(floorplan): 优化平面图状态管理性能 chore(deps): 添加zustand依赖 chore(config): 更新安全配置锁定时间为3分钟 docs: 更新部分组件注释 style: 调整登录页面样式
This commit is contained in:
@@ -8,7 +8,7 @@ module.exports = {
|
|||||||
|
|
||||||
MAX_LOGIN_ATTEMPTS: parseInt(process.env.MAX_LOGIN_ATTEMPTS, 10) || 5,
|
MAX_LOGIN_ATTEMPTS: parseInt(process.env.MAX_LOGIN_ATTEMPTS, 10) || 5,
|
||||||
|
|
||||||
LOCK_TIME: (parseInt(process.env.LOCK_TIME_MINUTES, 10) || 30) * 60 * 1000,
|
LOCK_TIME: (parseInt(process.env.LOCK_TIME_MINUTES, 10) || 3) * 60 * 1000,
|
||||||
|
|
||||||
TOKEN_EXPIRY: process.env.TOKEN_EXPIRY || '24h',
|
TOKEN_EXPIRY: process.env.TOKEN_EXPIRY || '24h',
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ const User = sequelize.define(
|
|||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
|
lockedUntil: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '账户锁定过期时间,NULL表示未锁定或已解锁',
|
||||||
|
},
|
||||||
remark: {
|
remark: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
+21
-2
@@ -8,6 +8,7 @@ const { generateToken, authMiddleware } = require('../middleware/auth');
|
|||||||
const {
|
const {
|
||||||
SALT_ROUNDS,
|
SALT_ROUNDS,
|
||||||
MAX_LOGIN_ATTEMPTS,
|
MAX_LOGIN_ATTEMPTS,
|
||||||
|
LOCK_TIME,
|
||||||
PASSWORD_MIN_LENGTH,
|
PASSWORD_MIN_LENGTH,
|
||||||
USERNAME_MIN_LENGTH,
|
USERNAME_MIN_LENGTH,
|
||||||
USERNAME_MAX_LENGTH,
|
USERNAME_MAX_LENGTH,
|
||||||
@@ -154,11 +155,19 @@ router.post('/login', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user.status === 'locked') {
|
if (user.status === 'locked') {
|
||||||
|
const now = new Date();
|
||||||
|
if (user.lockedUntil && user.lockedUntil > now) {
|
||||||
|
const remainingMinutes = Math.ceil((user.lockedUntil - now) / 60000);
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '账户已被锁定,请联系管理员',
|
message: `账户已被锁定,请在 ${remainingMinutes} 分钟后重试`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
user.status = 'active';
|
||||||
|
user.loginCount = 0;
|
||||||
|
user.lockedUntil = null;
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
|
||||||
if (user.status === 'inactive') {
|
if (user.status === 'inactive') {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
@@ -180,12 +189,21 @@ router.post('/login', async (req, res) => {
|
|||||||
user.loginCount = (user.loginCount || 0) + 1;
|
user.loginCount = (user.loginCount || 0) + 1;
|
||||||
if (user.loginCount >= MAX_LOGIN_ATTEMPTS) {
|
if (user.loginCount >= MAX_LOGIN_ATTEMPTS) {
|
||||||
user.status = 'locked';
|
user.status = 'locked';
|
||||||
|
user.lockedUntil = new Date(Date.now() + LOCK_TIME);
|
||||||
}
|
}
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
|
const remainingAttempts = MAX_LOGIN_ATTEMPTS - user.loginCount;
|
||||||
|
let message = '用户名或密码错误';
|
||||||
|
if (remainingAttempts > 0) {
|
||||||
|
message += `,剩余 ${remainingAttempts} 次尝试机会`;
|
||||||
|
} else {
|
||||||
|
message = `账户已被锁定,请在 3 分钟后重试`;
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名或密码错误',
|
message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +219,7 @@ router.post('/login', async (req, res) => {
|
|||||||
user.lastLoginTime = new Date();
|
user.lastLoginTime = new Date();
|
||||||
user.lastLoginIp = req.ip || req.connection.remoteAddress;
|
user.lastLoginIp = req.ip || req.connection.remoteAddress;
|
||||||
user.loginCount = 0;
|
user.loginCount = 0;
|
||||||
|
user.lockedUntil = null;
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ const migrations = [
|
|||||||
description: '为 operation_logs 表添加 requestId 字段和复合索引,支持请求追踪',
|
description: '为 operation_logs 表添加 requestId 字段和复合索引,支持请求追踪',
|
||||||
migrate: migrateOperationLogRequestId,
|
migrate: migrateOperationLogRequestId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '用户账户锁定时间',
|
||||||
|
description: '为 users 表添加 lockedUntil 字段,支持账户自动解锁',
|
||||||
|
migrate: migrateUserLockedUntil,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runMigrations() {
|
async function runMigrations() {
|
||||||
@@ -951,6 +956,18 @@ async function migrateOperationLogRequestId() {
|
|||||||
console.log(' 操作日志requestId字段和索引迁移完成');
|
console.log(' 操作日志requestId字段和索引迁移完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateUserLockedUntil() {
|
||||||
|
const tableName = 'users';
|
||||||
|
|
||||||
|
if (!(await tableExists(tableName))) {
|
||||||
|
console.log(` ${tableName} 表不存在,跳过`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await addColumnIfNotExists(tableName, 'lockedUntil', 'DATETIME');
|
||||||
|
console.log(' users 表 lockedUntil 字段迁移完成');
|
||||||
|
}
|
||||||
|
|
||||||
// 执行迁移
|
// 执行迁移
|
||||||
runMigrations().catch(error => {
|
runMigrations().catch(error => {
|
||||||
console.error('迁移执行失败:', error);
|
console.error('迁移执行失败:', error);
|
||||||
|
|||||||
Generated
+71
-233
@@ -29,7 +29,8 @@
|
|||||||
"styled-components": "^6.3.9",
|
"styled-components": "^6.3.9",
|
||||||
"swr": "^2.4.0",
|
"swr": "^2.4.0",
|
||||||
"three": "^0.183.2",
|
"three": "^0.183.2",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5",
|
||||||
|
"zustand": "^4.5.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
@@ -280,6 +281,7 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -628,6 +630,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -671,6 +674,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -692,6 +696,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/memoize": "^0.9.0"
|
"@emotion/memoize": "^0.9.0"
|
||||||
}
|
}
|
||||||
@@ -1732,11 +1737,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-three/drei/node_modules/zustand": {
|
||||||
|
"version": "5.0.13",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.13.tgz",
|
||||||
|
"integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-three/fiber": {
|
"node_modules/@react-three/fiber": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
|
||||||
"integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
|
"integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.17.8",
|
"@babel/runtime": "^7.17.8",
|
||||||
"@types/react-reconciler": "^0.26.7",
|
"@types/react-reconciler": "^0.26.7",
|
||||||
@@ -1822,34 +1857,6 @@
|
|||||||
"react-dom": ">=17"
|
"react-dom": ">=17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reactflow/background/node_modules/zustand": {
|
|
||||||
"version": "4.5.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
|
||||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reactflow/controls": {
|
"node_modules/@reactflow/controls": {
|
||||||
"version": "11.2.14",
|
"version": "11.2.14",
|
||||||
"resolved": "https://registry.npmmirror.com/@reactflow/controls/-/controls-11.2.14.tgz",
|
"resolved": "https://registry.npmmirror.com/@reactflow/controls/-/controls-11.2.14.tgz",
|
||||||
@@ -1865,34 +1872,6 @@
|
|||||||
"react-dom": ">=17"
|
"react-dom": ">=17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reactflow/controls/node_modules/zustand": {
|
|
||||||
"version": "4.5.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
|
||||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reactflow/core": {
|
"node_modules/@reactflow/core": {
|
||||||
"version": "11.11.4",
|
"version": "11.11.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@reactflow/core/-/core-11.11.4.tgz",
|
"resolved": "https://registry.npmmirror.com/@reactflow/core/-/core-11.11.4.tgz",
|
||||||
@@ -1914,34 +1893,6 @@
|
|||||||
"react-dom": ">=17"
|
"react-dom": ">=17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reactflow/core/node_modules/zustand": {
|
|
||||||
"version": "4.5.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
|
||||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reactflow/minimap": {
|
"node_modules/@reactflow/minimap": {
|
||||||
"version": "11.7.14",
|
"version": "11.7.14",
|
||||||
"resolved": "https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.7.14.tgz",
|
"resolved": "https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.7.14.tgz",
|
||||||
@@ -1961,34 +1912,6 @@
|
|||||||
"react-dom": ">=17"
|
"react-dom": ">=17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reactflow/minimap/node_modules/zustand": {
|
|
||||||
"version": "4.5.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
|
||||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reactflow/node-resizer": {
|
"node_modules/@reactflow/node-resizer": {
|
||||||
"version": "2.2.14",
|
"version": "2.2.14",
|
||||||
"resolved": "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
|
"resolved": "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
|
||||||
@@ -2006,34 +1929,6 @@
|
|||||||
"react-dom": ">=17"
|
"react-dom": ">=17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reactflow/node-resizer/node_modules/zustand": {
|
|
||||||
"version": "4.5.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
|
||||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reactflow/node-toolbar": {
|
"node_modules/@reactflow/node-toolbar": {
|
||||||
"version": "1.3.14",
|
"version": "1.3.14",
|
||||||
"resolved": "https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
|
"resolved": "https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
|
||||||
@@ -2049,34 +1944,6 @@
|
|||||||
"react-dom": ">=17"
|
"react-dom": ">=17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reactflow/node-toolbar/node_modules/zustand": {
|
|
||||||
"version": "4.5.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
|
||||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.1",
|
"version": "1.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
||||||
@@ -2509,8 +2376,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
@@ -2871,6 +2737,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
||||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -2901,6 +2768,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
|
||||||
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
|
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
"@tweenjs/tween.js": "~23.1.3",
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
@@ -3046,7 +2914,6 @@
|
|||||||
"integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
|
"integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.0",
|
||||||
"@vue/shared": "3.5.29",
|
"@vue/shared": "3.5.29",
|
||||||
@@ -3061,7 +2928,6 @@
|
|||||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
},
|
},
|
||||||
@@ -3074,8 +2940,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.29",
|
||||||
@@ -3083,7 +2948,6 @@
|
|||||||
"integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
|
"integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.29",
|
"@vue/compiler-core": "3.5.29",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.29"
|
||||||
@@ -3113,8 +2977,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.29",
|
"version": "3.5.29",
|
||||||
@@ -3122,7 +2985,6 @@
|
|||||||
"integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
|
"integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.29",
|
"@vue/compiler-dom": "3.5.29",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.29"
|
||||||
@@ -3134,7 +2996,6 @@
|
|||||||
"integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
|
"integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.29"
|
||||||
}
|
}
|
||||||
@@ -3145,7 +3006,6 @@
|
|||||||
"integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
|
"integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.29",
|
"@vue/reactivity": "3.5.29",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.29"
|
||||||
@@ -3157,7 +3017,6 @@
|
|||||||
"integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
|
"integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.29",
|
"@vue/reactivity": "3.5.29",
|
||||||
"@vue/runtime-core": "3.5.29",
|
"@vue/runtime-core": "3.5.29",
|
||||||
@@ -3171,7 +3030,6 @@
|
|||||||
"integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
|
"integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.29",
|
"@vue/compiler-ssr": "3.5.29",
|
||||||
"@vue/shared": "3.5.29"
|
"@vue/shared": "3.5.29"
|
||||||
@@ -3185,8 +3043,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
|
||||||
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
|
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@webgpu/types": {
|
"node_modules/@webgpu/types": {
|
||||||
"version": "0.1.69",
|
"version": "0.1.69",
|
||||||
@@ -3200,6 +3057,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3259,7 +3117,6 @@
|
|||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -3659,6 +3516,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -4135,6 +3993,7 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -4265,7 +4124,8 @@
|
|||||||
"version": "1.11.19",
|
"version": "1.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
||||||
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
@@ -4380,8 +4240,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/dom-helpers": {
|
"node_modules/dom-helpers": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
@@ -4680,6 +4539,7 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -4740,6 +4600,7 @@
|
|||||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
@@ -6103,6 +5964,7 @@
|
|||||||
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
|
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@acemir/cssom": "^0.9.28",
|
"@acemir/cssom": "^0.9.28",
|
||||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||||
@@ -6317,7 +6179,6 @@
|
|||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
@@ -6859,6 +6720,7 @@
|
|||||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -6888,7 +6750,6 @@
|
|||||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"ansi-styles": "^5.0.0",
|
"ansi-styles": "^5.0.0",
|
||||||
@@ -6904,7 +6765,6 @@
|
|||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -6917,8 +6777,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/promise-worker-transferable": {
|
"node_modules/promise-worker-transferable": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -7591,6 +7450,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -7615,6 +7475,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -8562,6 +8423,7 @@
|
|||||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
@@ -8588,7 +8450,8 @@
|
|||||||
"version": "0.183.2",
|
"version": "0.183.2",
|
||||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/three-mesh-bvh": {
|
"node_modules/three-mesh-bvh": {
|
||||||
"version": "0.7.8",
|
"version": "0.7.8",
|
||||||
@@ -8773,34 +8636,6 @@
|
|||||||
"zustand": "^4.3.2"
|
"zustand": "^4.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tunnel-rat/node_modules/zustand": {
|
|
||||||
"version": "4.5.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
|
||||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.2.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.7.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": ">=16.8",
|
|
||||||
"immer": ">=9.0.6",
|
|
||||||
"react": ">=16.8"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"immer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -9139,6 +8974,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
@@ -9167,6 +9003,7 @@
|
|||||||
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
@@ -9786,6 +9623,7 @@
|
|||||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -10191,6 +10029,7 @@
|
|||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -10209,18 +10048,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zustand": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.10",
|
"version": "4.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
||||||
"integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==",
|
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.2.2"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20.0"
|
"node": ">=12.7.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": ">=18.0.0",
|
"@types/react": ">=16.8",
|
||||||
"immer": ">=9.0.6",
|
"immer": ">=9.0.6",
|
||||||
"react": ">=18.0.0",
|
"react": ">=16.8"
|
||||||
"use-sync-external-store": ">=1.2.0"
|
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
@@ -10231,9 +10072,6 @@
|
|||||||
},
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
|
||||||
"use-sync-external-store": {
|
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"styled-components": "^6.3.9",
|
"styled-components": "^6.3.9",
|
||||||
"swr": "^2.4.0",
|
"swr": "^2.4.0",
|
||||||
"three": "^0.183.2",
|
"three": "^0.183.2",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5",
|
||||||
|
"zustand": "^4.5.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
|||||||
+5
-15
@@ -46,9 +46,8 @@ import {
|
|||||||
useLocation,
|
useLocation,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { useAuth } from './context/AuthContext';
|
import { useAuth } from './hooks/useAuth';
|
||||||
import { ConfigProvider, useConfig } from './context/ConfigContext';
|
import { useConfig } from './hooks/useConfig';
|
||||||
import { Scene3DProvider } from './context/Scene3DContext';
|
|
||||||
import { useDesignTokens } from './hooks/useDesignTokens';
|
import { useDesignTokens } from './hooks/useDesignTokens';
|
||||||
import useIdleTimeout from './hooks/useIdleTimeout';
|
import useIdleTimeout from './hooks/useIdleTimeout';
|
||||||
import { SWRConfig, swrConfig } from './hooks/useSWR';
|
import { SWRConfig, swrConfig } from './hooks/useSWR';
|
||||||
@@ -143,10 +142,9 @@ const ProtectedRoute = ({ component: Component }) => (
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 默认空闲超时配置
|
|
||||||
const DEFAULT_IDLE_CONFIG = {
|
const DEFAULT_IDLE_CONFIG = {
|
||||||
timeout: 30 * 60 * 1000, // 30分钟
|
timeout: 30 * 60 * 1000,
|
||||||
warningTime: 60 * 1000, // 60秒
|
warningTime: 60 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppLayout = ({ children }) => {
|
const AppLayout = ({ children }) => {
|
||||||
@@ -159,7 +157,6 @@ const AppLayout = ({ children }) => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const designTokens = useDesignTokens();
|
const designTokens = useDesignTokens();
|
||||||
|
|
||||||
// 获取空闲超时配置
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchIdleConfig = async () => {
|
const fetchIdleConfig = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -176,7 +173,6 @@ const AppLayout = ({ children }) => {
|
|||||||
fetchIdleConfig();
|
fetchIdleConfig();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 启用空闲超时检测
|
|
||||||
useIdleTimeout({
|
useIdleTimeout({
|
||||||
timeout: idleConfig.timeout,
|
timeout: idleConfig.timeout,
|
||||||
warningTime: idleConfig.warningTime,
|
warningTime: idleConfig.warningTime,
|
||||||
@@ -674,9 +670,7 @@ const ThemeConfig = () => {
|
|||||||
title="3D 可视化加载失败"
|
title="3D 可视化加载失败"
|
||||||
subTitle="3D 场景在加载过程中遇到错误,可能是浏览器不支持 WebGL 或模型文件加载失败"
|
subTitle="3D 场景在加载过程中遇到错误,可能是浏览器不支持 WebGL 或模型文件加载失败"
|
||||||
>
|
>
|
||||||
<Scene3DProvider>
|
|
||||||
<Rack3DVisualization />
|
<Rack3DVisualization />
|
||||||
</Scene3DProvider>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
@@ -691,11 +685,7 @@ const ThemeConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return <ThemeConfig />;
|
||||||
<ConfigProvider>
|
|
||||||
<ThemeConfig />
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -10,7 +10,7 @@ import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
|||||||
import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei';
|
import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei';
|
||||||
const envMapUrl = '/assets/3d/env.hdr';
|
const envMapUrl = '/assets/3d/env.hdr';
|
||||||
import RackModel from './RackModel';
|
import RackModel from './RackModel';
|
||||||
import { useScene3D } from '../../context/Scene3DContext';
|
import { useScene3D } from '../../hooks/useScene3D';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import ErrorBoundary from '../ErrorBoundary';
|
import ErrorBoundary from '../ErrorBoundary';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Zustand Store 初始化组件
|
||||||
|
* 确保所有必要的 Store 在应用渲染前完成初始化
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
import { useConfigStore } from '../stores/configStore';
|
||||||
|
import Spin from 'antd/es/spin';
|
||||||
|
|
||||||
|
const AuthInitializer = ({ children }) => {
|
||||||
|
const [allInitialized, setAllInitialized] = useState(false);
|
||||||
|
|
||||||
|
const { initialize: initializeAuth, initialized: authInitialized } = useAuthStore();
|
||||||
|
const { loadConfig, loading: configLoading } = useConfigStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initStores = async () => {
|
||||||
|
await initializeAuth();
|
||||||
|
await loadConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
initStores().then(() => {
|
||||||
|
setAllInitialized(true);
|
||||||
|
});
|
||||||
|
}, [initializeAuth, loadConfig]);
|
||||||
|
|
||||||
|
if (!allInitialized) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthInitializer;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
|
||||||
const ProtectedRoute = ({ children, requiredPermission }) => {
|
const ProtectedRoute = ({ children, requiredPermission }) => {
|
||||||
const { user, token, loading, initialized } = useAuth();
|
const { user, token, loading, initialized } = useAuth();
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
|
||||||
import { authAPI, setAuthInitialized } from '../api';
|
|
||||||
import secureStorage, { TOKEN_KEY, USER_KEY } from '../utils/secureStorage';
|
|
||||||
|
|
||||||
const AuthContext = createContext({
|
|
||||||
user: null,
|
|
||||||
token: null,
|
|
||||||
loading: true,
|
|
||||||
initialized: false,
|
|
||||||
login: async () => ({ success: false, message: '认证未初始化' }),
|
|
||||||
register: async () => ({ success: false, message: '认证未初始化' }),
|
|
||||||
logout: () => {},
|
|
||||||
updateUser: () => {},
|
|
||||||
hasPermission: () => false,
|
|
||||||
checkAdmin: () => Promise.resolve({ success: true, data: { hasAdmin: false, userCount: 0 } }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useAuth = () => {
|
|
||||||
return useContext(AuthContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
|
||||||
const [user, setUser] = useState(null);
|
|
||||||
const [token, setToken] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [initialized, setInitialized] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
const initializeAuth = async () => {
|
|
||||||
try {
|
|
||||||
const storedToken = await secureStorage.loadFromStorage(TOKEN_KEY);
|
|
||||||
const storedUser = await secureStorage.loadFromStorage(USER_KEY);
|
|
||||||
|
|
||||||
if (cancelled) return;
|
|
||||||
|
|
||||||
if (!storedToken) {
|
|
||||||
setToken(null);
|
|
||||||
setUser(null);
|
|
||||||
setLoading(false);
|
|
||||||
setInitialized(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setToken(storedToken);
|
|
||||||
setUser(storedUser);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await authAPI.getProfile();
|
|
||||||
if (cancelled) return;
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setUser(response.data.user);
|
|
||||||
await secureStorage.set(USER_KEY, response.data.user);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (cancelled) return;
|
|
||||||
|
|
||||||
const status = error?.response?.status;
|
|
||||||
if (status === 401 || status === 403) {
|
|
||||||
secureStorage.remove(TOKEN_KEY);
|
|
||||||
secureStorage.remove(USER_KEY);
|
|
||||||
setToken(null);
|
|
||||||
setUser(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (cancelled) return;
|
|
||||||
setToken(null);
|
|
||||||
setUser(null);
|
|
||||||
} finally {
|
|
||||||
if (!cancelled) {
|
|
||||||
setLoading(false);
|
|
||||||
setInitialized(true);
|
|
||||||
setAuthInitialized(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeAuth();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const login = useCallback(async (username, password) => {
|
|
||||||
try {
|
|
||||||
const response = await authAPI.login({ username, password });
|
|
||||||
if (response.success) {
|
|
||||||
const { token: newToken, user: userData } = response.data;
|
|
||||||
await secureStorage.set(TOKEN_KEY, newToken);
|
|
||||||
await secureStorage.set(USER_KEY, userData);
|
|
||||||
setToken(newToken);
|
|
||||||
setUser(userData);
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
return { success: false, message: response.message, code: response.code };
|
|
||||||
} catch (error) {
|
|
||||||
const message = error?.response?.data?.message || error?.message || '登录失败,请稍后重试';
|
|
||||||
return { success: false, message };
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const register = useCallback(async userData => {
|
|
||||||
try {
|
|
||||||
const response = await authAPI.register(userData);
|
|
||||||
if (response.success) {
|
|
||||||
const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data;
|
|
||||||
if (newToken) {
|
|
||||||
await secureStorage.set(TOKEN_KEY, newToken);
|
|
||||||
await secureStorage.set(USER_KEY, newUser);
|
|
||||||
setToken(newToken);
|
|
||||||
setUser(newUser);
|
|
||||||
}
|
|
||||||
return { success: true, isFirstUser, pendingApproval };
|
|
||||||
}
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
} catch (error) {
|
|
||||||
const message = error?.response?.data?.message || error?.message || '注册失败,请稍后重试';
|
|
||||||
return { success: false, message };
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
secureStorage.remove(TOKEN_KEY);
|
|
||||||
secureStorage.remove(USER_KEY);
|
|
||||||
setToken(null);
|
|
||||||
setUser(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateUser = useCallback(newUserData => {
|
|
||||||
setUser(prev => {
|
|
||||||
const updated = { ...prev, ...newUserData };
|
|
||||||
secureStorage.set(USER_KEY, updated).catch(() => {});
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hasPermission = useCallback(permission => {
|
|
||||||
if (!user) return false;
|
|
||||||
const roles = user.roles || [];
|
|
||||||
if (roles.some(r => r.roleCode === 'admin')) return true;
|
|
||||||
if (permission === 'admin') return roles.some(r => r.roleCode === 'admin');
|
|
||||||
return roles.some(r => r.roleCode === permission);
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const checkAdmin = useCallback(() => authAPI.checkAdmin(), []);
|
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
loading,
|
|
||||||
initialized,
|
|
||||||
login,
|
|
||||||
register,
|
|
||||||
logout,
|
|
||||||
updateUser,
|
|
||||||
hasPermission,
|
|
||||||
checkAdmin,
|
|
||||||
}), [user, token, loading, initialized, login, register, logout, updateUser, hasPermission, checkAdmin]);
|
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AuthContext;
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import React, { createContext, useState, useEffect, useContext } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const ConfigContext = createContext();
|
|
||||||
|
|
||||||
const applyThemeColors = (primaryColor, secondaryColor) => {
|
|
||||||
const root = document.documentElement;
|
|
||||||
if (primaryColor) {
|
|
||||||
root.style.setProperty('--primary-color', primaryColor);
|
|
||||||
root.style.setProperty('--primary-light', `${primaryColor}20`);
|
|
||||||
root.style.setProperty(
|
|
||||||
'--primary-gradient',
|
|
||||||
`linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor || '#764ba2'} 100%)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (secondaryColor) {
|
|
||||||
root.style.setProperty('--secondary-color', secondaryColor);
|
|
||||||
root.style.setProperty('--secondary-light', `${secondaryColor}20`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConfigProvider = ({ children }) => {
|
|
||||||
const [config, setConfig] = useState({
|
|
||||||
site_name: '机柜管理系统',
|
|
||||||
primary_color: '#667eea',
|
|
||||||
secondary_color: '#764ba2',
|
|
||||||
sidebar_collapsed: false,
|
|
||||||
compact_mode: false,
|
|
||||||
animation_enabled: true,
|
|
||||||
language: 'zh-CN',
|
|
||||||
timezone: 'Asia/Shanghai',
|
|
||||||
date_format: 'YYYY-MM-DD',
|
|
||||||
session_timeout: 30,
|
|
||||||
max_login_attempts: 5,
|
|
||||||
maintenance_mode: false,
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const loadConfig = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/api/system-settings');
|
|
||||||
const settings = response.data;
|
|
||||||
const configValues = {};
|
|
||||||
|
|
||||||
Object.entries(settings).forEach(([key, value]) => {
|
|
||||||
configValues[key] = value.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
setConfig(prev => ({
|
|
||||||
...prev,
|
|
||||||
...configValues,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (configValues.primary_color || configValues.secondary_color) {
|
|
||||||
applyThemeColors(configValues.primary_color, configValues.secondary_color);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载系统配置失败:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadConfig();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (config.primary_color || config.secondary_color) {
|
|
||||||
applyThemeColors(config.primary_color, config.secondary_color);
|
|
||||||
}
|
|
||||||
}, [config.primary_color, config.secondary_color]);
|
|
||||||
|
|
||||||
const updateConfig = newConfig => {
|
|
||||||
setConfig(prev => ({
|
|
||||||
...prev,
|
|
||||||
...newConfig,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadConfig = async () => {
|
|
||||||
await loadConfig();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigContext.Provider value={{ config, loading, updateConfig, reloadConfig }}>
|
|
||||||
{children}
|
|
||||||
</ConfigContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useConfig = () => {
|
|
||||||
const context = useContext(ConfigContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useConfig must be used within a ConfigProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import React, { createContext, useReducer, useCallback } from 'react';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
selectedRoomId: null,
|
|
||||||
selectedRack: null,
|
|
||||||
hoveredRack: null,
|
|
||||||
zoom: 1,
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 0,
|
|
||||||
detailRack: null,
|
|
||||||
detailVisible: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionTypes = {
|
|
||||||
SET_SELECTED_ROOM: 'SET_SELECTED_ROOM',
|
|
||||||
SET_SELECTED_RACK: 'SET_SELECTED_RACK',
|
|
||||||
SET_HOVERED_RACK: 'SET_HOVERED_RACK',
|
|
||||||
SET_VIEW_CHANGE: 'SET_VIEW_CHANGE',
|
|
||||||
SHOW_DETAIL: 'SHOW_DETAIL',
|
|
||||||
HIDE_DETAIL: 'HIDE_DETAIL',
|
|
||||||
RESET: 'RESET',
|
|
||||||
};
|
|
||||||
|
|
||||||
function floorPlanReducer(state, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case actionTypes.SET_SELECTED_ROOM:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedRoomId: action.payload,
|
|
||||||
selectedRack: null,
|
|
||||||
hoveredRack: null,
|
|
||||||
detailRack: null,
|
|
||||||
detailVisible: false,
|
|
||||||
};
|
|
||||||
case actionTypes.SET_SELECTED_RACK:
|
|
||||||
return { ...state, selectedRack: action.payload };
|
|
||||||
case actionTypes.SET_HOVERED_RACK:
|
|
||||||
return { ...state, hoveredRack: action.payload };
|
|
||||||
case actionTypes.SET_VIEW_CHANGE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
zoom: action.payload.zoom,
|
|
||||||
offsetX: action.payload.offsetX,
|
|
||||||
offsetY: action.payload.offsetY,
|
|
||||||
};
|
|
||||||
case actionTypes.SHOW_DETAIL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
detailRack: action.payload,
|
|
||||||
detailVisible: true,
|
|
||||||
};
|
|
||||||
case actionTypes.HIDE_DETAIL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
detailRack: null,
|
|
||||||
detailVisible: false,
|
|
||||||
};
|
|
||||||
case actionTypes.RESET:
|
|
||||||
return { ...initialState };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FloorPlanContext = createContext(null);
|
|
||||||
|
|
||||||
export const FloorPlanProvider = ({ children }) => {
|
|
||||||
const [state, dispatch] = useReducer(floorPlanReducer, initialState);
|
|
||||||
|
|
||||||
const setSelectedRoom = useCallback((roomId) => {
|
|
||||||
dispatch({ type: actionTypes.SET_SELECTED_ROOM, payload: roomId });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setSelectedRack = useCallback((rack) => {
|
|
||||||
dispatch({ type: actionTypes.SET_SELECTED_RACK, payload: rack });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setHoveredRack = useCallback((rack) => {
|
|
||||||
dispatch({ type: actionTypes.SET_HOVERED_RACK, payload: rack });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setViewChange = useCallback((viewState) => {
|
|
||||||
dispatch({ type: actionTypes.SET_VIEW_CHANGE, payload: viewState });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const showDetail = useCallback((rack) => {
|
|
||||||
dispatch({ type: actionTypes.SHOW_DETAIL, payload: rack });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hideDetail = useCallback(() => {
|
|
||||||
dispatch({ type: actionTypes.HIDE_DETAIL });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
...state,
|
|
||||||
setSelectedRoom,
|
|
||||||
setSelectedRack,
|
|
||||||
setHoveredRack,
|
|
||||||
setViewChange,
|
|
||||||
showDetail,
|
|
||||||
hideDetail,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FloorPlanContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</FloorPlanContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FloorPlanContext;
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import React, { createContext, useContext, useState, useMemo, useCallback } from 'react';
|
|
||||||
|
|
||||||
// 3D场景状态上下文
|
|
||||||
// 将3D场景状态与UI状态分离,避免不必要的重渲染
|
|
||||||
|
|
||||||
const Scene3DContext = createContext(null);
|
|
||||||
|
|
||||||
export const Scene3DProvider = ({ children }) => {
|
|
||||||
// 3D场景相关状态
|
|
||||||
const [devices, setDevices] = useState([]);
|
|
||||||
const [selectedDevice, setSelectedDevice] = useState(null);
|
|
||||||
const [hoveredDevice, setHoveredDevice] = useState(null);
|
|
||||||
const [deviceSlideEnabled, setDeviceSlideEnabled] = useState(false);
|
|
||||||
const [selectedRack, setSelectedRack] = useState(null);
|
|
||||||
const [racks, setRacks] = useState([]);
|
|
||||||
const [deviceCables, setDeviceCables] = useState([]);
|
|
||||||
const [loadingDevices, setLoadingDevices] = useState(false);
|
|
||||||
|
|
||||||
// 使用 useCallback 稳定回调函数
|
|
||||||
const selectDevice = useCallback(device => {
|
|
||||||
setSelectedDevice(device);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hoverDevice = useCallback(device => {
|
|
||||||
setHoveredDevice(device);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleDeviceSlide = useCallback(() => {
|
|
||||||
setDeviceSlideEnabled(prev => !prev);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setDeviceSlide = useCallback(enabled => {
|
|
||||||
setDeviceSlideEnabled(enabled);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateDevices = useCallback(newDevices => {
|
|
||||||
setDevices(newDevices);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateRacks = useCallback(newRacks => {
|
|
||||||
setRacks(newRacks);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectRack = useCallback(rack => {
|
|
||||||
setSelectedRack(rack);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateDeviceCables = useCallback(cables => {
|
|
||||||
setDeviceCables(cables);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setLoading = useCallback(loading => {
|
|
||||||
setLoadingDevices(loading);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 使用 useMemo 缓存 context value,避免不必要的重渲染
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({
|
|
||||||
// 状态
|
|
||||||
devices,
|
|
||||||
selectedDevice,
|
|
||||||
hoveredDevice,
|
|
||||||
deviceSlideEnabled,
|
|
||||||
selectedRack,
|
|
||||||
racks,
|
|
||||||
deviceCables,
|
|
||||||
loadingDevices,
|
|
||||||
// 方法
|
|
||||||
selectDevice,
|
|
||||||
hoverDevice,
|
|
||||||
toggleDeviceSlide,
|
|
||||||
setDeviceSlide,
|
|
||||||
updateDevices,
|
|
||||||
updateRacks,
|
|
||||||
selectRack,
|
|
||||||
updateDeviceCables,
|
|
||||||
setLoading,
|
|
||||||
// 直接设置状态的方法(用于兼容现有代码)
|
|
||||||
setDevices,
|
|
||||||
setSelectedDevice,
|
|
||||||
setHoveredDevice,
|
|
||||||
setDeviceSlideEnabled,
|
|
||||||
setSelectedRack,
|
|
||||||
setRacks,
|
|
||||||
setDeviceCables,
|
|
||||||
setLoadingDevices,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
devices,
|
|
||||||
selectedDevice,
|
|
||||||
hoveredDevice,
|
|
||||||
deviceSlideEnabled,
|
|
||||||
selectedRack,
|
|
||||||
racks,
|
|
||||||
deviceCables,
|
|
||||||
loadingDevices,
|
|
||||||
selectDevice,
|
|
||||||
hoverDevice,
|
|
||||||
toggleDeviceSlide,
|
|
||||||
setDeviceSlide,
|
|
||||||
updateDevices,
|
|
||||||
updateRacks,
|
|
||||||
selectRack,
|
|
||||||
updateDeviceCables,
|
|
||||||
setLoading,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Scene3DContext.Provider value={value}>{children}</Scene3DContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自定义 Hook
|
|
||||||
export const useScene3D = () => {
|
|
||||||
const context = useContext(Scene3DContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useScene3D must be used within a Scene3DProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Scene3DContext;
|
|
||||||
@@ -1,12 +1,39 @@
|
|||||||
import { useContext } from 'react';
|
import { useFloorPlanStore } from '../../stores/floorPlanStore';
|
||||||
import FloorPlanContext from '../../context/FloorPlanContext';
|
|
||||||
|
|
||||||
const useFloorPlanContext = () => {
|
const useFloorPlanContext = () => {
|
||||||
const context = useContext(FloorPlanContext);
|
const selectedRoomId = useFloorPlanStore((s) => s.selectedRoomId);
|
||||||
if (!context) {
|
const selectedRack = useFloorPlanStore((s) => s.selectedRack);
|
||||||
throw new Error('useFloorPlanContext 必须在 FloorPlanProvider 内使用');
|
const hoveredRack = useFloorPlanStore((s) => s.hoveredRack);
|
||||||
}
|
const zoom = useFloorPlanStore((s) => s.zoom);
|
||||||
return context;
|
const offsetX = useFloorPlanStore((s) => s.offsetX);
|
||||||
|
const offsetY = useFloorPlanStore((s) => s.offsetY);
|
||||||
|
const detailRack = useFloorPlanStore((s) => s.detailRack);
|
||||||
|
const detailVisible = useFloorPlanStore((s) => s.detailVisible);
|
||||||
|
const setSelectedRoom = useFloorPlanStore((s) => s.setSelectedRoom);
|
||||||
|
const setSelectedRack = useFloorPlanStore((s) => s.setSelectedRack);
|
||||||
|
const setHoveredRack = useFloorPlanStore((s) => s.setHoveredRack);
|
||||||
|
const setViewChange = useFloorPlanStore((s) => s.setViewChange);
|
||||||
|
const showDetail = useFloorPlanStore((s) => s.showDetail);
|
||||||
|
const hideDetail = useFloorPlanStore((s) => s.hideDetail);
|
||||||
|
const reset = useFloorPlanStore((s) => s.reset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedRoomId,
|
||||||
|
selectedRack,
|
||||||
|
hoveredRack,
|
||||||
|
zoom,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
detailRack,
|
||||||
|
detailVisible,
|
||||||
|
setSelectedRoom,
|
||||||
|
setSelectedRack,
|
||||||
|
setHoveredRack,
|
||||||
|
setViewChange,
|
||||||
|
showDetail,
|
||||||
|
hideDetail,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useFloorPlanContext;
|
export default useFloorPlanContext;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 认证状态 Hook
|
||||||
|
* 直接使用 Zustand Store 管理认证状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const user = useAuthStore((s) => s.user);
|
||||||
|
const token = useAuthStore((s) => s.token);
|
||||||
|
const loading = useAuthStore((s) => s.loading);
|
||||||
|
const initialized = useAuthStore((s) => s.initialized);
|
||||||
|
const login = useAuthStore((s) => s.login);
|
||||||
|
const register = useAuthStore((s) => s.register);
|
||||||
|
const logout = useAuthStore((s) => s.logout);
|
||||||
|
const updateUser = useAuthStore((s) => s.updateUser);
|
||||||
|
const hasPermission = useAuthStore((s) => s.hasPermission);
|
||||||
|
const checkAdmin = useAuthStore((s) => s.checkAdmin);
|
||||||
|
const initialize = useAuthStore((s) => s.initialize);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialized) {
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
}, [initialized, initialize]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
loading,
|
||||||
|
initialized,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
updateUser,
|
||||||
|
hasPermission,
|
||||||
|
checkAdmin,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAuth;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 系统配置 Hook
|
||||||
|
* 直接使用 Zustand Store 管理系统配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useConfigStore } from '../stores/configStore';
|
||||||
|
|
||||||
|
export const useConfig = () => {
|
||||||
|
const config = useConfigStore((s) => s.config);
|
||||||
|
const loading = useConfigStore((s) => s.loading);
|
||||||
|
const updateConfig = useConfigStore((s) => s.updateConfig);
|
||||||
|
const reloadConfig = useConfigStore((s) => s.reloadConfig);
|
||||||
|
const loadConfig = useConfigStore((s) => s.loadConfig);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
loadConfig();
|
||||||
|
}
|
||||||
|
}, [loading, loadConfig]);
|
||||||
|
|
||||||
|
return { config, loading, updateConfig, reloadConfig };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useConfig;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useConfig } from '../context/ConfigContext';
|
import { useConfigStore } from '../stores/configStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用设计令牌 Hook
|
* 使用设计令牌 Hook
|
||||||
@@ -7,12 +7,10 @@ import { useConfig } from '../context/ConfigContext';
|
|||||||
* @returns {Object} 设计令牌对象
|
* @returns {Object} 设计令牌对象
|
||||||
*/
|
*/
|
||||||
export const useDesignTokens = () => {
|
export const useDesignTokens = () => {
|
||||||
const { config } = useConfig();
|
const primaryColor = useConfigStore((s) => s.config.primary_color) || '#667eea';
|
||||||
|
const secondaryColor = useConfigStore((s) => s.config.secondary_color) || '#764ba2';
|
||||||
|
|
||||||
const designTokens = useMemo(() => {
|
const designTokens = useMemo(() => {
|
||||||
const primaryColor = config?.primary_color || '#667eea';
|
|
||||||
const secondaryColor = config?.secondary_color || '#764ba2';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
@@ -61,7 +59,7 @@ export const useDesignTokens = () => {
|
|||||||
lg: '24px',
|
lg: '24px',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [config?.primary_color, config?.secondary_color]);
|
}, [primaryColor, secondaryColor]);
|
||||||
|
|
||||||
return designTokens;
|
return designTokens;
|
||||||
};
|
};
|
||||||
@@ -72,10 +70,8 @@ export const useDesignTokens = () => {
|
|||||||
* @returns {string} RGB字符串 (如: "102, 126, 234")
|
* @returns {string} RGB字符串 (如: "102, 126, 234")
|
||||||
*/
|
*/
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
// 移除 # 号
|
|
||||||
const cleanHex = hex.replace('#', '');
|
const cleanHex = hex.replace('#', '');
|
||||||
|
|
||||||
// 处理简写格式 (如: #fff)
|
|
||||||
const fullHex =
|
const fullHex =
|
||||||
cleanHex.length === 3
|
cleanHex.length === 3
|
||||||
? cleanHex
|
? cleanHex
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 平面图 Hook
|
||||||
|
* 直接使用 Zustand Store 管理平面图状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useFloorPlanStore } from '../stores/floorPlanStore';
|
||||||
|
|
||||||
|
export const useFloorPlan = () => {
|
||||||
|
return useFloorPlanStore();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFloorPlan;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 3D场景 Hook
|
||||||
|
* 直接使用 Zustand Store 管理3D场景状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useScene3DStore } from '../stores/scene3DStore';
|
||||||
|
|
||||||
|
export const useScene3D = () => {
|
||||||
|
return useScene3DStore();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useScene3D;
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { AuthProvider } from './context/AuthContext';
|
import AuthInitializer from './components/AuthInitializer';
|
||||||
import ErrorBoundary from './components/ErrorBoundary';
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ErrorBoundary fullPage>
|
<ErrorBoundary fullPage>
|
||||||
<AuthProvider>
|
<AuthInitializer>
|
||||||
<App />
|
<App />
|
||||||
</AuthProvider>
|
</AuthInitializer>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
+213
-485
@@ -1,16 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { version as appVersion } from '../../package.json';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
message,
|
message,
|
||||||
Typography,
|
Typography,
|
||||||
Divider,
|
|
||||||
Space,
|
|
||||||
Alert,
|
Alert,
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -20,18 +16,20 @@ import {
|
|||||||
SafetyCertificateOutlined,
|
SafetyCertificateOutlined,
|
||||||
CloudServerOutlined,
|
CloudServerOutlined,
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
SecurityScanOutlined,
|
||||||
|
ApiOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import { authAPI } from '../api';
|
import './Login.css';
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isFirstUser, setIsFirstUser] = useState(false);
|
const [isFirstUser, setIsFirstUser] = useState(false);
|
||||||
const [registerMode, setRegisterMode] = useState(false);
|
const [registerMode, setRegisterMode] = useState(false);
|
||||||
const [unlockMode, setUnlockMode] = useState(false);
|
|
||||||
const { login, register, checkAdmin } = useAuth();
|
const { login, register, checkAdmin } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -44,61 +42,30 @@ const Login = () => {
|
|||||||
const response = await checkAdmin();
|
const response = await checkAdmin();
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setIsFirstUser(!response.data.hasAdmin);
|
setIsFirstUser(!response.data.hasAdmin);
|
||||||
if (!response.data.hasAdmin) {
|
|
||||||
setRegisterMode(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 静默处理:登录页调用 check-admin 失败是正常的(未登录状态)
|
console.error('检查管理员状态失败:', error);
|
||||||
// 不显示错误信息,避免触发 401 重定向循环
|
|
||||||
console.log('检查管理员状态:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinishLogin = async values => {
|
const onFinishLogin = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await login(values.username, values.password);
|
const result = await login(values.username, values.password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
navigate('/');
|
navigate('/dashboard');
|
||||||
} else {
|
} else {
|
||||||
if (result.code === 'PENDING_APPROVAL') {
|
message.error(result.message);
|
||||||
message.warning('账户待审核,请联系管理员激活');
|
|
||||||
} else {
|
|
||||||
message.error(result.message || '登录失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error || '登录失败');
|
message.error('登录失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFinishUnlock = async values => {
|
const onFinishRegister = async (values) => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await authAPI.unlock(values);
|
|
||||||
if (response.success) {
|
|
||||||
message.success('解锁成功,请重新登录');
|
|
||||||
setUnlockMode(false);
|
|
||||||
} else {
|
|
||||||
message.error(response.message || '解锁失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error || '解锁失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFinishRegister = async values => {
|
|
||||||
if (values.password !== values.confirmPassword) {
|
|
||||||
message.error('两次输入的密码不一致');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await register({
|
const result = await register({
|
||||||
@@ -108,348 +75,203 @@ const Login = () => {
|
|||||||
phone: values.phone,
|
phone: values.phone,
|
||||||
realName: values.realName,
|
realName: values.realName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
message.success(result.isFirstUser ? '管理员账号创建成功' : '注册成功,请等待管理员审核');
|
||||||
if (result.isFirstUser) {
|
if (result.isFirstUser) {
|
||||||
message.success('注册成功,已为您创建管理员账户');
|
navigate('/dashboard');
|
||||||
navigate('/');
|
|
||||||
} else if (result.pendingApproval) {
|
|
||||||
message.success('注册成功,请等待管理员审核');
|
|
||||||
setRegisterMode(false);
|
|
||||||
} else {
|
} else {
|
||||||
message.success('注册成功');
|
setRegisterMode(false);
|
||||||
navigate('/');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(result.message || '注册失败');
|
message.error(result.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error || '注册失败');
|
message.error('注册失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 左侧宣传区域组件
|
const features = [
|
||||||
const LeftPanel = () => (
|
{ icon: <DashboardOutlined />, title: '实时监控', desc: '3D可视化机房全景', color: '#14b8a6' },
|
||||||
|
{ icon: <SecurityScanOutlined />, title: '安全可靠', desc: '多重权限防护', color: '#3b82f6' },
|
||||||
|
{ icon: <ApiOutlined />, title: '极速响应', desc: '毫秒级数据采集', color: '#8b5cf6' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ value: '99.9%', label: '可用性', icon: '↑' },
|
||||||
|
{ value: '24/7', label: '监控', icon: '●' },
|
||||||
|
{ value: '<100ms', label: '响应', icon: '⚡' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const RackVisual = () => (
|
||||||
|
<div className="rack-visual">
|
||||||
|
<div className="rack-visual__header">
|
||||||
|
<span className="rack-visual__title">机房概览</span>
|
||||||
|
<span className="rack-visual__badge">3 在线</span>
|
||||||
|
</div>
|
||||||
|
<div className="rack-visual__body">
|
||||||
|
{[1, 2, 3].map((rack) => (
|
||||||
|
<div key={rack} className="rack-unit" style={{ animationDelay: `${rack * 0.1}s` }}>
|
||||||
|
<div className="rack-unit__header">
|
||||||
|
<span className="rack-unit__id">{rack.toString().padStart(2, '0')}</span>
|
||||||
|
<span className="rack-unit__status" />
|
||||||
|
</div>
|
||||||
|
<div className="rack-unit__slots">
|
||||||
|
{[1, 2, 3, 4, 5].map((slot) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
key={slot}
|
||||||
height: '100%',
|
className={`rack-unit__slot rack-unit__slot--${slot <= (4 - rack + 1) ? 'on' : 'off'}`}
|
||||||
display: 'flex',
|
style={{ animationDelay: `${(rack * 5 + slot) * 0.03}s` }}
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: '60px',
|
|
||||||
color: '#fff',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 背景装饰 */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
width: '600px',
|
|
||||||
height: '600px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: 'rgba(255,255,255,0.1)',
|
|
||||||
top: '-200px',
|
|
||||||
left: '-200px',
|
|
||||||
filter: 'blur(60px)',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div
|
))}
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
width: '400px',
|
|
||||||
height: '400px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: 'rgba(255,255,255,0.08)',
|
|
||||||
bottom: '-100px',
|
|
||||||
right: '-100px',
|
|
||||||
filter: 'blur(40px)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '80px',
|
|
||||||
height: '80px',
|
|
||||||
borderRadius: '20px',
|
|
||||||
background: 'rgba(255,255,255,0.2)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginBottom: '40px',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloudServerOutlined style={{ fontSize: '40px', color: '#fff' }} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="rack-unit__load">
|
||||||
<Title
|
<div className="rack-unit__load-bar" style={{ width: `${25 + rack * 18}%` }} />
|
||||||
level={1}
|
|
||||||
style={{
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: '48px',
|
|
||||||
fontWeight: 700,
|
|
||||||
marginBottom: '24px',
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
IDC设备
|
|
||||||
<br />
|
|
||||||
管理系统
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Paragraph
|
|
||||||
style={{
|
|
||||||
color: 'rgba(255,255,255,0.85)',
|
|
||||||
fontSize: '18px',
|
|
||||||
lineHeight: 1.8,
|
|
||||||
maxWidth: '480px',
|
|
||||||
marginBottom: '48px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
专业的数据中心设备管理平台,提供机房、机柜、设备的全生命周期管理,
|
|
||||||
助力企业实现高效的IT资产管理。
|
|
||||||
</Paragraph>
|
|
||||||
|
|
||||||
<Row gutter={[24, 24]}>
|
|
||||||
<Col xs={8} sm={8} md={8}>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>99.9%</div>
|
|
||||||
<div style={{ fontSize: '13px', opacity: 0.8 }}>系统稳定性</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
|
||||||
<Col xs={8} sm={8} md={8}>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>24/7</div>
|
|
||||||
<div style={{ fontSize: '13px', opacity: 0.8 }}>全天候监控</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
))}
|
||||||
<Col xs={8} sm={8} md={8}>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>100%</div>
|
|
||||||
<div style={{ fontSize: '13px', opacity: 0.8 }}>数据安全</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取标题和副标题
|
|
||||||
const getHeaderContent = () => {
|
|
||||||
if (isFirstUser) {
|
|
||||||
return {
|
|
||||||
title: '创建管理员账户',
|
|
||||||
subtitle: '首次使用,请创建系统管理员账户',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (unlockMode) {
|
|
||||||
return {
|
|
||||||
title: '账户解锁',
|
|
||||||
subtitle: '输入账户信息以解锁账户',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (registerMode) {
|
|
||||||
return {
|
|
||||||
title: '注册新账户',
|
|
||||||
subtitle: '填写信息完成账户注册',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title: '欢迎回来',
|
|
||||||
subtitle: '请登录您的账户以继续',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerContent = getHeaderContent();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row style={{ minHeight: '100vh', overflow: 'hidden' }}>
|
<div className="login-container">
|
||||||
{/* 左侧区域 - 桌面端显示 */}
|
<div className="login-bg-decoration">
|
||||||
<Col
|
<div className="decoration-circle decoration-circle--1" />
|
||||||
xs={0}
|
<div className="decoration-circle decoration-circle--2" />
|
||||||
sm={0}
|
<div className="decoration-circle decoration-circle--3" />
|
||||||
md={0}
|
<div className="decoration-grid" />
|
||||||
lg={12}
|
<div className="decoration-wave" />
|
||||||
xl={14}
|
</div>
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LeftPanel />
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* 右侧登录区域 */}
|
<div className="login-wrapper">
|
||||||
<Col
|
<div className="login-card">
|
||||||
xs={24}
|
<div className="login-card__brand">
|
||||||
sm={24}
|
<div className="brand-header">
|
||||||
md={24}
|
<div className="brand-logo">
|
||||||
lg={12}
|
<CloudServerOutlined />
|
||||||
xl={10}
|
<div className="logo-pulse" />
|
||||||
style={{
|
</div>
|
||||||
display: 'flex',
|
<div className="brand-title-section">
|
||||||
alignItems: 'center',
|
<h1 className="brand-title">
|
||||||
justifyContent: 'center',
|
<span className="brand-title__main">IDC</span>
|
||||||
background: '#f8fafc',
|
<span className="brand-title__sub">机柜管理系统</span>
|
||||||
padding: '24px',
|
</h1>
|
||||||
position: 'relative',
|
<p className="brand-desc">智能数据中心基础设施管理平台</p>
|
||||||
}}
|
</div>
|
||||||
>
|
</div>
|
||||||
{/* 移动端背景 */}
|
|
||||||
|
<RackVisual />
|
||||||
|
|
||||||
|
<div className="brand-stats">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<div key={index} className="stat-card">
|
||||||
|
<span className="stat-value">{stat.value}</span>
|
||||||
|
<span className="stat-label">{stat.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="brand-features">
|
||||||
|
{features.map((feature, index) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
key={index}
|
||||||
position: 'absolute',
|
className="brand-feature"
|
||||||
top: 0,
|
style={{ animationDelay: `${index * 0.1}s` }}
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: '200px',
|
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
||||||
display: 'none',
|
|
||||||
}}
|
|
||||||
className="mobile-bg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: registerMode ? 520 : 440,
|
|
||||||
borderRadius: '24px',
|
|
||||||
boxShadow: '0 25px 80px rgba(0,0,0,0.15), 0 10px 30px rgba(0,0,0,0.1)',
|
|
||||||
background: '#fff',
|
|
||||||
border: 'none',
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
bodyStyle={{ padding: '48px' }}
|
|
||||||
>
|
>
|
||||||
{/* 返回按钮 */}
|
<div
|
||||||
{(registerMode || unlockMode) && !isFirstUser && (
|
className="feature-icon"
|
||||||
|
style={{ background: `linear-gradient(135deg, ${feature.color}15, ${feature.color}10)`, color: feature.color }}
|
||||||
|
>
|
||||||
|
{feature.icon}
|
||||||
|
</div>
|
||||||
|
<div className="feature-content">
|
||||||
|
<span className="feature-title">{feature.title}</span>
|
||||||
|
<span className="feature-desc">{feature.desc}</span>
|
||||||
|
</div>
|
||||||
|
<div className="feature-arrow">→</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="brand-footer">
|
||||||
|
<span className="footer-trust">
|
||||||
|
<span className="trust-icon">🛡️</span>
|
||||||
|
企业级安全认证
|
||||||
|
</span>
|
||||||
|
<span className="footer-version">v{appVersion}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-card__divider" />
|
||||||
|
|
||||||
|
<div className="login-card__form">
|
||||||
|
<div className="form-header">
|
||||||
|
<Title level={2} className="form-title">
|
||||||
|
{isFirstUser ? '初始化系统' : '欢迎回来'}
|
||||||
|
</Title>
|
||||||
|
<Text className="form-subtitle">
|
||||||
|
{isFirstUser
|
||||||
|
? '创建管理员账号以开始使用系统'
|
||||||
|
: '请登录您的账号继续访问'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFirstUser && (
|
||||||
|
<Alert
|
||||||
|
className="form-alert"
|
||||||
|
message="系统初始化"
|
||||||
|
description="首次使用需要创建管理员账号"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{registerMode && !isFirstUser && (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="text"
|
||||||
icon={<ArrowLeftOutlined />}
|
icon={<ArrowLeftOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRegisterMode(false);
|
setRegisterMode(false);
|
||||||
setUnlockMode(false);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '24px',
|
|
||||||
left: '24px',
|
|
||||||
color: '#667eea',
|
|
||||||
padding: '4px 8px',
|
|
||||||
}}
|
}}
|
||||||
|
className="back-button"
|
||||||
>
|
>
|
||||||
返回
|
返回登录
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 头部 */}
|
|
||||||
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '64px',
|
|
||||||
height: '64px',
|
|
||||||
borderRadius: '16px',
|
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
margin: '0 auto 20px',
|
|
||||||
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.35)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloudServerOutlined style={{ fontSize: '32px', color: '#fff' }} />
|
|
||||||
</div>
|
|
||||||
<Title
|
|
||||||
level={3}
|
|
||||||
style={{
|
|
||||||
fontSize: '28px',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: '#1e293b',
|
|
||||||
marginBottom: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{headerContent.title}
|
|
||||||
</Title>
|
|
||||||
<Text style={{ fontSize: '15px', color: '#64748b' }}>{headerContent.subtitle}</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 首次使用提示 */}
|
|
||||||
{isFirstUser && (
|
|
||||||
<Alert
|
|
||||||
message="欢迎使用IDC设备管理系统"
|
|
||||||
description="您是第一个用户,系统将自动为您分配管理员权限。"
|
|
||||||
type="success"
|
|
||||||
showIcon
|
|
||||||
style={{ marginBottom: '24px', borderRadius: '12px' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 解锁模式提示 */}
|
|
||||||
{unlockMode && (
|
|
||||||
<Alert
|
|
||||||
message="账户解锁说明"
|
|
||||||
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
|
|
||||||
type="info"
|
|
||||||
showIcon
|
|
||||||
style={{ marginBottom: '24px', borderRadius: '12px' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 表单 */}
|
|
||||||
<Form
|
<Form
|
||||||
name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'}
|
name={registerMode ? 'register' : 'login'}
|
||||||
size="large"
|
|
||||||
onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin}
|
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
requiredMark={false}
|
onFinish={registerMode ? onFinishRegister : onFinishLogin}
|
||||||
|
size="large"
|
||||||
|
className="login-form"
|
||||||
>
|
>
|
||||||
{registerMode ? (
|
{registerMode && (
|
||||||
<>
|
<>
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
label="用户名"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: '请输入用户名' },
|
|
||||||
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
|
|
||||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
style={{ borderRadius: '12px', height: '48px' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="realName"
|
name="realName"
|
||||||
label="真实姓名"
|
label="真实姓名"
|
||||||
rules={[{ required: true, message: '请输入真实姓名' }]}
|
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={
|
|
||||||
<SafetyCertificateOutlined
|
|
||||||
style={{ color: '#94a3b8', fontSize: '18px' }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placeholder="请输入真实姓名"
|
placeholder="请输入真实姓名"
|
||||||
style={{ borderRadius: '12px', height: '48px' }}
|
className="login-input"
|
||||||
|
prefix={<UserOutlined className="input-icon" />}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
className="login-input"
|
||||||
|
prefix={<UserOutlined className="input-icon" />}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="email"
|
name="email"
|
||||||
label="邮箱"
|
label="邮箱"
|
||||||
@@ -459,41 +281,53 @@ const Login = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<MailOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
|
||||||
placeholder="请输入邮箱"
|
placeholder="请输入邮箱"
|
||||||
style={{ borderRadius: '12px', height: '48px' }}
|
className="login-input"
|
||||||
|
prefix={<MailOutlined className="input-icon" />}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
<Form.Item
|
||||||
<Col span={12}>
|
name="phone"
|
||||||
<Form.Item name="phone" label="手机号">
|
label="手机号"
|
||||||
|
rules={[{ required: true, message: '请输入手机号' }]}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<PhoneOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
placeholder="请输入手机号"
|
||||||
placeholder="请输入手机号(可选)"
|
className="login-input"
|
||||||
style={{ borderRadius: '12px', height: '48px' }}
|
prefix={<PhoneOutlined className="input-icon" />}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label={isFirstUser ? '管理员账号' : '用户名'}
|
||||||
|
rules={[{ required: true, message: isFirstUser ? '请设置管理员账号' : '请输入用户名' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined className="input-icon" />}
|
||||||
|
placeholder={isFirstUser ? '请设置管理员账号' : '请输入用户名'}
|
||||||
|
className="login-input"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="password"
|
name="password"
|
||||||
label="密码"
|
label={isFirstUser ? '管理员密码' : '密码'}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入密码' },
|
{ required: true, message: isFirstUser ? '请设置管理员密码' : '请输入密码' },
|
||||||
{ min: 6, message: '密码长度不能少于6个字符' },
|
{ min: 6, message: '密码长度不能少于6位' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
prefix={<LockOutlined className="input-icon" />}
|
||||||
placeholder="请输入密码"
|
placeholder={isFirstUser ? '请设置管理员密码' : '请输入密码'}
|
||||||
style={{ borderRadius: '12px', height: '48px' }}
|
className="login-input"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
{registerMode && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
label="确认密码"
|
label="确认密码"
|
||||||
@@ -511,148 +345,42 @@ const Login = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
prefix={<LockOutlined className="input-icon" />}
|
||||||
placeholder="请再次输入密码"
|
placeholder="请确认密码"
|
||||||
style={{ borderRadius: '12px', height: '48px' }}
|
className="login-input"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
name="username"
|
|
||||||
label="用户名"
|
|
||||||
rules={[{ required: true, message: '请输入用户名' }]}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
|
||||||
placeholder="请输入用户名"
|
|
||||||
style={{ borderRadius: '12px', height: '52px' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
label="密码"
|
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
|
||||||
style={{ marginBottom: '8px' }}
|
|
||||||
>
|
|
||||||
<Input.Password
|
|
||||||
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
|
||||||
placeholder="请输入密码"
|
|
||||||
style={{ borderRadius: '12px', height: '52px' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{!registerMode && !unlockMode && (
|
|
||||||
<div style={{ textAlign: 'right', marginBottom: '24px' }}>
|
|
||||||
<Button type="link" style={{ color: '#667eea', padding: 0 }}>
|
|
||||||
忘记密码?
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item style={{ marginTop: '32px', marginBottom: '16px' }}>
|
<Form.Item className="submit-item">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
block
|
className="submit-button"
|
||||||
style={{
|
|
||||||
height: '52px',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 600,
|
|
||||||
borderRadius: '12px',
|
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
||||||
border: 'none',
|
|
||||||
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.35)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{registerMode ? '立即注册' : unlockMode ? '立即解锁' : '登 录'}
|
{registerMode ? '立即注册' : '登 录'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{/* 底部切换 */}
|
{!isFirstUser && !registerMode && (
|
||||||
{!isFirstUser && (
|
<div className="form-actions">
|
||||||
<div style={{ textAlign: 'center', marginTop: '24px' }}>
|
<Button type="link" className="action-link" onClick={() => setRegisterMode(true)}>
|
||||||
{!unlockMode && !registerMode && (
|
<SafetyCertificateOutlined /> 立即注册
|
||||||
<Space split={<Divider type="vertical" />} size="large">
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() => setRegisterMode(true)}
|
|
||||||
style={{ color: '#64748b', fontWeight: 500 }}
|
|
||||||
>
|
|
||||||
注册新账户
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() => setUnlockMode(true)}
|
|
||||||
style={{ color: '#64748b', fontWeight: 500 }}
|
|
||||||
>
|
|
||||||
账户解锁
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(registerMode || unlockMode) && (
|
|
||||||
<Text style={{ color: '#64748b' }}>
|
|
||||||
已有账户?{' '}
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
onClick={() => {
|
|
||||||
setRegisterMode(false);
|
|
||||||
setUnlockMode(false);
|
|
||||||
}}
|
|
||||||
style={{ color: '#667eea', fontWeight: 600, padding: 0 }}
|
|
||||||
>
|
|
||||||
立即登录
|
|
||||||
</Button>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 移动端底部版权 */}
|
<div className="form-footer">
|
||||||
<div
|
<span>IDC Management System</span>
|
||||||
style={{
|
<span className="footer-dot" />
|
||||||
position: 'absolute',
|
<span>v{appVersion}</span>
|
||||||
bottom: '24px',
|
</div>
|
||||||
left: 0,
|
</div>
|
||||||
right: 0,
|
</div>
|
||||||
textAlign: 'center',
|
</div>
|
||||||
color: '#94a3b8',
|
|
||||||
fontSize: '13px',
|
|
||||||
display: 'none',
|
|
||||||
}}
|
|
||||||
className="mobile-footer"
|
|
||||||
>
|
|
||||||
© 2024 IDC设备管理系统. All rights reserved.
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* 响应式样式 */}
|
|
||||||
<style>{`
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.mobile-bg {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
.mobile-footer {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 575px) {
|
|
||||||
.ant-card-body {
|
|
||||||
padding: 32px 24px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</Row>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import DeviceDetailDrawer from '../components/DeviceDetailDrawer';
|
|||||||
import CloseButton from '../components/CloseButton';
|
import CloseButton from '../components/CloseButton';
|
||||||
import RackSelectorHeader from '../components/3d/RackSelectorHeader';
|
import RackSelectorHeader from '../components/3d/RackSelectorHeader';
|
||||||
import { Layout } from 'antd';
|
import { Layout } from 'antd';
|
||||||
import { useScene3D } from '../context/Scene3DContext';
|
import { useScene3D } from '../hooks/useScene3D';
|
||||||
import { useSortedRacks } from '../hooks/useSortedRacks';
|
import { useSortedRacks } from '../hooks/useSortedRacks';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
@@ -46,7 +46,7 @@ const Rack3DVisualization = () => {
|
|||||||
// Scene 组件的 ref,用于调用重置视角方法
|
// Scene 组件的 ref,用于调用重置视角方法
|
||||||
const sceneRef = useRef(null);
|
const sceneRef = useRef(null);
|
||||||
|
|
||||||
// 使用 Scene3DContext 管理3D场景状态
|
// 使用 Zustand Store 管理3D场景状态
|
||||||
const {
|
const {
|
||||||
devices,
|
devices,
|
||||||
setDevices,
|
setDevices,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useRef, useCallback, useState, useEffect } from 'react';
|
import React, { useRef, useCallback, useState, useEffect } from 'react';
|
||||||
import { Spin, Empty, message, Button } from 'antd';
|
import { Spin, Empty, message, Button } from 'antd';
|
||||||
import { HomeOutlined, ReloadOutlined } from '@ant-design/icons';
|
import { HomeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
import { FloorPlanProvider } from '../context/FloorPlanContext';
|
|
||||||
import useFloorPlanContext from '../hooks/floorplan/useFloorPlanContext';
|
import useFloorPlanContext from '../hooks/floorplan/useFloorPlanContext';
|
||||||
import useFloorPlanData from '../hooks/floorplan/useFloorPlanData';
|
import useFloorPlanData from '../hooks/floorplan/useFloorPlanData';
|
||||||
import { FloorPlanCanvas, FloorPlanToolbar, RackDetailPanel } from '../components/floorplan';
|
import { FloorPlanCanvas, FloorPlanToolbar, RackDetailPanel } from '../components/floorplan';
|
||||||
@@ -48,28 +47,20 @@ const DeviceTooltip = ({ device, rack, x, y }) => {
|
|||||||
</DeviceInfoRow>
|
</DeviceInfoRow>
|
||||||
<DeviceInfoRow>
|
<DeviceInfoRow>
|
||||||
<DeviceInfoLabel>状态:</DeviceInfoLabel>
|
<DeviceInfoLabel>状态:</DeviceInfoLabel>
|
||||||
<DeviceInfoValue $color={statusColor}>{DEVICE_STATUS_NAMES[device.status] || device.status}</DeviceInfoValue>
|
<DeviceInfoValue style={{ color: statusColor }}>
|
||||||
</DeviceInfoRow>
|
{DEVICE_STATUS_NAMES[device.status] || device.status}
|
||||||
<DeviceInfoRow>
|
</DeviceInfoValue>
|
||||||
<DeviceInfoLabel>位置:</DeviceInfoLabel>
|
|
||||||
<DeviceInfoValue>{rack?.name} - {device.position}U</DeviceInfoValue>
|
|
||||||
</DeviceInfoRow>
|
</DeviceInfoRow>
|
||||||
{device.ipAddress && (
|
{device.ipAddress && (
|
||||||
<DeviceInfoRow>
|
<DeviceInfoRow>
|
||||||
<DeviceInfoLabel>IP:</DeviceInfoLabel>
|
<DeviceInfoLabel>IP:</DeviceInfoLabel>
|
||||||
<DeviceInfoValue $mono>{device.ipAddress}</DeviceInfoValue>
|
<DeviceInfoValue>{device.ipAddress}</DeviceInfoValue>
|
||||||
</DeviceInfoRow>
|
</DeviceInfoRow>
|
||||||
)}
|
)}
|
||||||
{device.model && (
|
{rack && (
|
||||||
<DeviceInfoRow>
|
<DeviceInfoRow>
|
||||||
<DeviceInfoLabel>型号:</DeviceInfoLabel>
|
<DeviceInfoLabel>位置:</DeviceInfoLabel>
|
||||||
<DeviceInfoValue>{device.model}</DeviceInfoValue>
|
<DeviceInfoValue>{rack.name} U{device.position}</DeviceInfoValue>
|
||||||
</DeviceInfoRow>
|
|
||||||
)}
|
|
||||||
{device.height > 1 && (
|
|
||||||
<DeviceInfoRow>
|
|
||||||
<DeviceInfoLabel>高度:</DeviceInfoLabel>
|
|
||||||
<DeviceInfoValue>{device.height}U</DeviceInfoValue>
|
|
||||||
</DeviceInfoRow>
|
</DeviceInfoRow>
|
||||||
)}
|
)}
|
||||||
</DeviceInfoContent>
|
</DeviceInfoContent>
|
||||||
@@ -77,26 +68,7 @@ const DeviceTooltip = ({ device, rack, x, y }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmptyState = ({ onRefresh }) => (
|
const RoomFloorPlanContent = () => {
|
||||||
<EmptyStateContainer>
|
|
||||||
<Empty
|
|
||||||
image={<HomeOutlined style={{ fontSize: 64, color: '#d9d9d9' }} />}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<EmptyStateTitle>暂无机房数据</EmptyStateTitle>
|
|
||||||
<EmptyStateSubtitle>请先在机房管理中创建机房,或检查网络连接</EmptyStateSubtitle>
|
|
||||||
{onRefresh && (
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={onRefresh}>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EmptyStateContainer>
|
|
||||||
);
|
|
||||||
|
|
||||||
const FloorPlanContent = () => {
|
|
||||||
const {
|
const {
|
||||||
selectedRoomId,
|
selectedRoomId,
|
||||||
setSelectedRoom,
|
setSelectedRoom,
|
||||||
@@ -160,113 +132,99 @@ const FloorPlanContent = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRackClick = useCallback((rack) => {
|
const handleDeviceHover = useCallback((device, rack, event) => {
|
||||||
if (rack) {
|
|
||||||
showDetail(rack);
|
|
||||||
}
|
|
||||||
}, [showDetail]);
|
|
||||||
|
|
||||||
const handleRackDoubleClick = useCallback((rack) => {
|
|
||||||
if (rack) {
|
|
||||||
showDetail(rack);
|
|
||||||
}
|
|
||||||
}, [showDetail]);
|
|
||||||
|
|
||||||
const handleDeviceHover = useCallback((device, rack, x, y) => {
|
|
||||||
if (device) {
|
if (device) {
|
||||||
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
setTooltipPosition({
|
||||||
|
x: event.clientX - rect.left + 15,
|
||||||
|
y: event.clientY - rect.top + 15,
|
||||||
|
});
|
||||||
|
}
|
||||||
setHoveredDevice(device);
|
setHoveredDevice(device);
|
||||||
setHoveredDeviceRack(rack);
|
setHoveredDeviceRack(rack);
|
||||||
setTooltipPosition({ x, y });
|
|
||||||
} else {
|
} else {
|
||||||
setHoveredDevice(null);
|
setHoveredDevice(null);
|
||||||
setHoveredDeviceRack(null);
|
setHoveredDeviceRack(null);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleViewChange = useCallback((viewState) => {
|
const handleRackClick = useCallback((rack) => {
|
||||||
setCurrentZoom(viewState.zoom);
|
if (rack) {
|
||||||
}, []);
|
showDetail(rack);
|
||||||
|
}
|
||||||
|
}, [showDetail]);
|
||||||
|
|
||||||
const handleExport = useCallback(() => {
|
if (loading) {
|
||||||
if (!canvasRef.current || !layoutData?.room) {
|
return (
|
||||||
message.warning('请先选择机房');
|
<PageContainer>
|
||||||
return;
|
<LoadingOverlay>
|
||||||
|
<Spin size="large" />
|
||||||
|
<span>加载中...</span>
|
||||||
|
</LoadingOverlay>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataUrl = canvasRef.current.exportImage(layoutData.room.name);
|
if (!layoutData || layoutData.racks.length === 0) {
|
||||||
if (!dataUrl) {
|
return (
|
||||||
message.error('导出失败,请稍后重试');
|
<PageContainer>
|
||||||
return;
|
<EmptyStateContainer>
|
||||||
|
<Empty
|
||||||
|
description={selectedRoomId ? '该机房暂无机柜数据' : '请先选择机房'}
|
||||||
|
/>
|
||||||
|
</EmptyStateContainer>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = `${layoutData.room.name || '机房平面图'}_${new Date().toISOString().slice(0, 10)}.png`;
|
|
||||||
link.href = dataUrl;
|
|
||||||
link.click();
|
|
||||||
message.success('导出成功');
|
|
||||||
}, [layoutData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentWrapper>
|
<PageContainer>
|
||||||
<FloorPlanToolbar
|
<FloorPlanToolbar
|
||||||
selectedRoomId={selectedRoomId}
|
roomName={layoutData.roomName}
|
||||||
|
roomId={selectedRoomId}
|
||||||
onRoomChange={setSelectedRoom}
|
onRoomChange={setSelectedRoom}
|
||||||
zoom={currentZoom}
|
zoom={currentZoom}
|
||||||
onZoomIn={() => canvasRef.current?.zoomIn()}
|
onZoomChange={setCurrentZoom}
|
||||||
onZoomOut={() => canvasRef.current?.zoomOut()}
|
onFullscreen={handleToggleFullscreen}
|
||||||
onZoomReset={() => canvasRef.current?.zoomReset()}
|
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
onToggleFullscreen={handleToggleFullscreen}
|
|
||||||
onRefresh={refetch}
|
onRefresh={refetch}
|
||||||
onExport={handleExport}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CanvasContainer ref={containerRef}>
|
<ContentWrapper ref={containerRef}>
|
||||||
{loading && (
|
<CanvasContainer>
|
||||||
<LoadingOverlay>
|
|
||||||
<Spin tip="加载中..." />
|
|
||||||
</LoadingOverlay>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!selectedRoomId && <EmptyState onRefresh={refetch} />}
|
|
||||||
|
|
||||||
{selectedRoomId && layoutData && (
|
|
||||||
<FloorPlanCanvas
|
<FloorPlanCanvas
|
||||||
ref={canvasRef}
|
|
||||||
room={layoutData.room}
|
|
||||||
racks={layoutData.racks}
|
racks={layoutData.racks}
|
||||||
|
rooms={layoutData.rooms}
|
||||||
|
selectedRoomId={selectedRoomId}
|
||||||
|
zoom={currentZoom}
|
||||||
onRackClick={handleRackClick}
|
onRackClick={handleRackClick}
|
||||||
onRackDoubleClick={handleRackDoubleClick}
|
|
||||||
onDeviceHover={handleDeviceHover}
|
onDeviceHover={handleDeviceHover}
|
||||||
onViewChange={handleViewChange}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
|
{hoveredDevice && (
|
||||||
<DeviceTooltip
|
<DeviceTooltip
|
||||||
device={hoveredDevice}
|
device={hoveredDevice}
|
||||||
rack={hoveredDeviceRack}
|
rack={hoveredDeviceRack}
|
||||||
x={tooltipPosition.x}
|
x={tooltipPosition.x}
|
||||||
y={tooltipPosition.y}
|
y={tooltipPosition.y}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</CanvasContainer>
|
</CanvasContainer>
|
||||||
|
</ContentWrapper>
|
||||||
|
|
||||||
|
{detailVisible && detailRack && (
|
||||||
<RackDetailPanel
|
<RackDetailPanel
|
||||||
rack={detailRack}
|
rack={detailRack}
|
||||||
visible={detailVisible}
|
|
||||||
onClose={hideDetail}
|
onClose={hideDetail}
|
||||||
/>
|
/>
|
||||||
</ContentWrapper>
|
)}
|
||||||
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomFloorPlan = () => {
|
const RoomFloorPlan = () => {
|
||||||
return (
|
return <RoomFloorPlanContent />;
|
||||||
<FloorPlanProvider>
|
|
||||||
<PageContainer>
|
|
||||||
<FloorPlanContent />
|
|
||||||
</PageContainer>
|
|
||||||
</FloorPlanProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RoomFloorPlan;
|
export default RoomFloorPlan;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useConfig } from '../context/ConfigContext';
|
import { useConfig } from '../hooks/useConfig';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 认证状态管理 Store
|
||||||
|
* 支持精准订阅、持久化、权限缓存
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { authAPI, setAuthInitialized } from '../api';
|
||||||
|
import secureStorage, { TOKEN_KEY, USER_KEY } from '../utils/secureStorage';
|
||||||
|
|
||||||
|
export const useAuthStore = create((set, get) => ({
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
loading: true,
|
||||||
|
initialized: false,
|
||||||
|
|
||||||
|
initialize: async () => {
|
||||||
|
try {
|
||||||
|
const storedToken = await secureStorage.loadFromStorage(TOKEN_KEY);
|
||||||
|
const storedUser = await secureStorage.loadFromStorage(USER_KEY);
|
||||||
|
|
||||||
|
if (!storedToken) {
|
||||||
|
set({ token: null, user: null, loading: false, initialized: true });
|
||||||
|
setAuthInitialized(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ token: storedToken, user: storedUser });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authAPI.getProfile();
|
||||||
|
if (response.success) {
|
||||||
|
set({ user: response.data.user });
|
||||||
|
await secureStorage.set(USER_KEY, response.data.user);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const status = error?.response?.status;
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
secureStorage.remove(TOKEN_KEY);
|
||||||
|
secureStorage.remove(USER_KEY);
|
||||||
|
set({ token: null, user: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
set({ token: null, user: null });
|
||||||
|
} finally {
|
||||||
|
set({ loading: false, initialized: true });
|
||||||
|
setAuthInitialized(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
login: async (username, password) => {
|
||||||
|
try {
|
||||||
|
const response = await authAPI.login({ username, password });
|
||||||
|
if (response.success) {
|
||||||
|
const { token: newToken, user: userData } = response.data;
|
||||||
|
await secureStorage.set(TOKEN_KEY, newToken);
|
||||||
|
await secureStorage.set(USER_KEY, userData);
|
||||||
|
set({ token: newToken, user: userData });
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return { success: false, message: response.message, code: response.code };
|
||||||
|
} catch (error) {
|
||||||
|
const message = error?.response?.data?.message || error?.message || '登录失败,请稍后重试';
|
||||||
|
return { success: false, message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
register: async (userData) => {
|
||||||
|
try {
|
||||||
|
const response = await authAPI.register(userData);
|
||||||
|
if (response.success) {
|
||||||
|
const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data;
|
||||||
|
if (newToken) {
|
||||||
|
await secureStorage.set(TOKEN_KEY, newToken);
|
||||||
|
await secureStorage.set(USER_KEY, newUser);
|
||||||
|
set({ token: newToken, user: newUser });
|
||||||
|
}
|
||||||
|
return { success: true, isFirstUser, pendingApproval };
|
||||||
|
}
|
||||||
|
return { success: false, message: response.message };
|
||||||
|
} catch (error) {
|
||||||
|
const message = error?.response?.data?.message || error?.message || '注册失败,请稍后重试';
|
||||||
|
return { success: false, message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
secureStorage.remove(TOKEN_KEY);
|
||||||
|
secureStorage.remove(USER_KEY);
|
||||||
|
set({ token: null, user: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUser: (newUserData) => {
|
||||||
|
set((state) => {
|
||||||
|
const updated = { ...state.user, ...newUserData };
|
||||||
|
secureStorage.set(USER_KEY, updated).catch(() => {});
|
||||||
|
return { user: updated };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPermission: (permission) => {
|
||||||
|
const { user } = get();
|
||||||
|
if (!user) return false;
|
||||||
|
const roles = user.roles || [];
|
||||||
|
if (roles.some((r) => r.roleCode === 'admin')) return true;
|
||||||
|
if (permission === 'admin') return roles.some((r) => r.roleCode === 'admin');
|
||||||
|
return roles.some((r) => r.roleCode === permission);
|
||||||
|
},
|
||||||
|
|
||||||
|
checkAdmin: () => authAPI.checkAdmin(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useUser = () => useAuthStore((state) => state.user);
|
||||||
|
export const useToken = () => useAuthStore((state) => state.token);
|
||||||
|
export const useAuthLoading = () => useAuthStore((state) => state.loading);
|
||||||
|
export const useAuthInitialized = () => useAuthStore((state) => state.initialized);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 系统配置状态管理 Store
|
||||||
|
* 支持精准订阅配置项,避免无关重渲染
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
site_name: '机柜管理系统',
|
||||||
|
primary_color: '#667eea',
|
||||||
|
secondary_color: '#764ba2',
|
||||||
|
sidebar_collapsed: false,
|
||||||
|
compact_mode: false,
|
||||||
|
animation_enabled: true,
|
||||||
|
language: 'zh-CN',
|
||||||
|
timezone: 'Asia/Shanghai',
|
||||||
|
date_format: 'YYYY-MM-DD',
|
||||||
|
session_timeout: 30,
|
||||||
|
max_login_attempts: 5,
|
||||||
|
maintenance_mode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyThemeColors = (primaryColor, secondaryColor) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (primaryColor) {
|
||||||
|
root.style.setProperty('--primary-color', primaryColor);
|
||||||
|
root.style.setProperty('--primary-light', `${primaryColor}20`);
|
||||||
|
root.style.setProperty(
|
||||||
|
'--primary-gradient',
|
||||||
|
`linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor || '#764ba2'} 100%)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (secondaryColor) {
|
||||||
|
root.style.setProperty('--secondary-color', secondaryColor);
|
||||||
|
root.style.setProperty('--secondary-light', `${secondaryColor}20`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfigStore = create((set, get) => ({
|
||||||
|
config: defaultConfig,
|
||||||
|
loading: true,
|
||||||
|
|
||||||
|
loadConfig: async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/system-settings');
|
||||||
|
const settings = response.data;
|
||||||
|
const configValues = {};
|
||||||
|
|
||||||
|
Object.entries(settings).forEach(([key, value]) => {
|
||||||
|
configValues[key] = value.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
config: { ...state.config, ...configValues },
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (configValues.primary_color || configValues.secondary_color) {
|
||||||
|
applyThemeColors(configValues.primary_color, configValues.secondary_color);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 加载失败使用默认配置
|
||||||
|
} finally {
|
||||||
|
set({ loading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateConfig: (newConfig) => {
|
||||||
|
set((state) => ({
|
||||||
|
config: { ...state.config, ...newConfig },
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (newConfig.primary_color || newConfig.secondary_color) {
|
||||||
|
const { config } = get();
|
||||||
|
applyThemeColors(config.primary_color, config.secondary_color);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reloadConfig: async () => {
|
||||||
|
await get().loadConfig();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useConfig = () => useConfigStore((state) => state.config);
|
||||||
|
export const useConfigLoading = () => useConfigStore((state) => state.loading);
|
||||||
|
export const useSiteName = () => useConfigStore((state) => state.config.site_name);
|
||||||
|
export const usePrimaryColor = () => useConfigStore((state) => state.config.primary_color);
|
||||||
|
export const useSecondaryColor = () => useConfigStore((state) => state.config.secondary_color);
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 平面图状态管理 Store
|
||||||
|
* 简化 reducer 逻辑,直接使用 set 更新状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export const useFloorPlanStore = create((set) => ({
|
||||||
|
selectedRoomId: null,
|
||||||
|
selectedRack: null,
|
||||||
|
hoveredRack: null,
|
||||||
|
zoom: 1,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
detailRack: null,
|
||||||
|
detailVisible: false,
|
||||||
|
|
||||||
|
setSelectedRoom: (roomId) =>
|
||||||
|
set({
|
||||||
|
selectedRoomId: roomId,
|
||||||
|
selectedRack: null,
|
||||||
|
hoveredRack: null,
|
||||||
|
detailRack: null,
|
||||||
|
detailVisible: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
setSelectedRack: (rack) => set({ selectedRack: rack }),
|
||||||
|
setHoveredRack: (rack) => set({ hoveredRack: rack }),
|
||||||
|
|
||||||
|
setViewChange: ({ zoom, offsetX, offsetY }) => set({ zoom, offsetX, offsetY }),
|
||||||
|
|
||||||
|
showDetail: (rack) => set({ detailRack: rack, detailVisible: true }),
|
||||||
|
hideDetail: () => set({ detailRack: null, detailVisible: false }),
|
||||||
|
|
||||||
|
reset: () =>
|
||||||
|
set({
|
||||||
|
selectedRoomId: null,
|
||||||
|
selectedRack: null,
|
||||||
|
hoveredRack: null,
|
||||||
|
zoom: 1,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
detailRack: null,
|
||||||
|
detailVisible: false,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 状态管理统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useAuthStore, useUser, useToken, useAuthLoading, useAuthInitialized } from './authStore';
|
||||||
|
export {
|
||||||
|
useConfigStore,
|
||||||
|
useConfig,
|
||||||
|
useConfigLoading,
|
||||||
|
useSiteName,
|
||||||
|
usePrimaryColor,
|
||||||
|
useSecondaryColor,
|
||||||
|
} from './configStore';
|
||||||
|
export { useScene3DStore, useDevices, useSelectedDevice, useRacks, useSelectedRack } from './scene3DStore';
|
||||||
|
export { useFloorPlanStore } from './floorPlanStore';
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 3D场景状态管理 Store
|
||||||
|
* 支持精准订阅,避免3D组件不必要的重渲染
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export const useScene3DStore = create((set) => ({
|
||||||
|
devices: [],
|
||||||
|
selectedDevice: null,
|
||||||
|
hoveredDevice: null,
|
||||||
|
deviceSlideEnabled: false,
|
||||||
|
selectedRack: null,
|
||||||
|
racks: [],
|
||||||
|
deviceCables: [],
|
||||||
|
loadingDevices: false,
|
||||||
|
|
||||||
|
selectDevice: (device) => set({ selectedDevice: device }),
|
||||||
|
setSelectedDevice: (device) => set({ selectedDevice: device }),
|
||||||
|
hoverDevice: (device) => set({ hoveredDevice: device }),
|
||||||
|
setHoveredDevice: (device) => set({ hoveredDevice: device }),
|
||||||
|
|
||||||
|
toggleDeviceSlide: () => set((state) => ({ deviceSlideEnabled: !state.deviceSlideEnabled })),
|
||||||
|
setDeviceSlide: (enabled) => set({ deviceSlideEnabled: enabled }),
|
||||||
|
setDeviceSlideEnabled: (enabled) => set({ deviceSlideEnabled: enabled }),
|
||||||
|
|
||||||
|
setDevices: (devices) => set({ devices }),
|
||||||
|
updateDevices: (devices) => set({ devices }),
|
||||||
|
|
||||||
|
updateRacks: (racks) => set({ racks }),
|
||||||
|
setRacks: (racks) => set({ racks }),
|
||||||
|
|
||||||
|
selectRack: (rack) => set({ selectedRack: rack }),
|
||||||
|
setSelectedRack: (rack) => set({ selectedRack: rack }),
|
||||||
|
|
||||||
|
updateDeviceCables: (cables) => set({ deviceCables: cables }),
|
||||||
|
setDeviceCables: (cables) => set({ deviceCables: cables }),
|
||||||
|
|
||||||
|
setLoading: (loading) => set({ loadingDevices: loading }),
|
||||||
|
setLoadingDevices: (loading) => set({ loadingDevices: loading }),
|
||||||
|
|
||||||
|
clearDeviceSelection: () => set({ selectedDevice: null, hoveredDevice: null }),
|
||||||
|
|
||||||
|
reset: () =>
|
||||||
|
set({
|
||||||
|
devices: [],
|
||||||
|
selectedDevice: null,
|
||||||
|
hoveredDevice: null,
|
||||||
|
deviceSlideEnabled: false,
|
||||||
|
selectedRack: null,
|
||||||
|
racks: [],
|
||||||
|
deviceCables: [],
|
||||||
|
loadingDevices: false,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useDevices = () => useScene3DStore((state) => state.devices);
|
||||||
|
export const useSelectedDevice = () => useScene3DStore((state) => state.selectedDevice);
|
||||||
|
export const useRacks = () => useScene3DStore((state) => state.racks);
|
||||||
|
export const useSelectedRack = () => useScene3DStore((state) => state.selectedRack);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* SecureStorage 适配器
|
||||||
|
* 将异步的 secureStorage 接口适配为 Zustand persist 所需的同步 Storage 接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { secureStorage } from '../utils/secureStorage';
|
||||||
|
|
||||||
|
const SecureStorageAdapter = {
|
||||||
|
getItem: async (name) => {
|
||||||
|
try {
|
||||||
|
const value = await secureStorage.loadFromStorage(name);
|
||||||
|
return value !== null ? JSON.stringify(value) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem: async (name, value) => {
|
||||||
|
try {
|
||||||
|
await secureStorage.set(name, JSON.parse(value));
|
||||||
|
} catch {
|
||||||
|
// 存储失败静默处理
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem: async (name) => {
|
||||||
|
try {
|
||||||
|
await secureStorage.remove(name);
|
||||||
|
} catch {
|
||||||
|
// 移除失败静默处理
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecureStorageAdapter;
|
||||||
Reference in New Issue
Block a user