From 54b98ac6fd8ba2373fa7cabb52ad1f1396d0a4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=9C=B1?= <10714957+xiao-zhu245@user.noreply.gitee.com> Date: Tue, 8 Jul 2025 21:51:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +- client/package-lock.json | 1077 ++++++++++++++++++++- client/package.json | 21 +- client/src/App.tsx | 78 +- client/src/components/FileContextMenu.tsx | 134 +++ client/src/components/FileDialogs.tsx | 288 ++++++ client/src/components/FileGridItem.tsx | 155 +++ client/src/components/Layout.tsx | 4 +- client/src/components/MonacoEditor.tsx | 161 +++ client/src/index.css | 92 ++ client/src/pages/FileManagerPage.tsx | 605 ++++++++++++ client/src/stores/fileStore.ts | 253 +++++ client/src/types/file.ts | 33 + client/src/types/index.ts | 5 +- client/src/utils/fileApi.ts | 125 +++ client/src/utils/format.ts | 81 ++ client/src/utils/monaco.ts | 8 + client/src/utils/pathUtils.ts | 168 ++++ client/vite.config.ts | 4 + install-deps.md | 13 + server/src/index.ts | 2 + 报错.txt | 76 -- 22 files changed, 3275 insertions(+), 139 deletions(-) create mode 100644 client/src/components/FileContextMenu.tsx create mode 100644 client/src/components/FileDialogs.tsx create mode 100644 client/src/components/FileGridItem.tsx create mode 100644 client/src/components/MonacoEditor.tsx create mode 100644 client/src/pages/FileManagerPage.tsx create mode 100644 client/src/stores/fileStore.ts create mode 100644 client/src/types/file.ts create mode 100644 client/src/utils/fileApi.ts create mode 100644 client/src/utils/format.ts create mode 100644 client/src/utils/monaco.ts create mode 100644 client/src/utils/pathUtils.ts create mode 100644 install-deps.md diff --git a/README.md b/README.md index 91cab2d..29519dc 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ -使用React框架写一个游戏面板,后端为node.js 目前只需要三个页面即可 首页 终端(需要实现功能) 设置 要同时兼容Windows和Linux -目前后端已经存在了,你需要写一个美观漂亮并且符合游戏风格的面板 -要支持全局深色和浅色模式更改 -# 配置文件 -统一保存在 server/data 目录下 +再写一个文件管理功能 使用了 Ant Design (antd) 作为基础UI组件库 使用Monaco Editor (VS Code的编辑器内核): +其中Monaco Editor 应当在面板中集成 +```json +"@monaco-editor/react": "^4.6.0", +"monaco-editor": "^0.45.0" +``` +```ts +import loader from '@monaco-editor/loader'; +import * as monaco from 'monaco-editor'; -# 终端 -需要做成拓展性很强,因为在后续功能需要调用此终端 需要做到灵活调用,并且终端需要在刷新网页时仍然为刷新网页前的状态和所有命令记录,由于后端pty已经是一个编译好的模块可以直接通过进程获取具体信息 具体你可以查看后端代码 -要支持终端的所有交互 +// 配置@monaco-editor/react使用本地的monaco-editor包而不是CDN +loader.config({ monaco }); -# 登录 -需要实现用户登录,jwt密钥不要采用硬编 而是通过每次启动后随机生成到配置文件 \ No newline at end of file +export default loader; +``` +- 导入本地安装的 monaco-editor 包 +- 使用 loader.config({ monaco }) 告诉 @monaco-editor/react 使用本地的monaco实例而不是从CDN加载 + +要支持文件的新建、删除、重命名、上传、下载、查看文件内容、切换目录等基础功能 并且支持右键文件的功能 + +文件管理要支持多选批量操作 做成九宫格形式优先展示文件夹之后再展示文件 要支持路径的输入和识别 默认路径应当设置在程序运行路径下 + +所有需要用户输入交互操作 专门做一个对话框或表单 要求具备动画 \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 0ab2478..6659806 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,19 +8,22 @@ "name": "gsm3-client", "version": "1.0.0", "dependencies": { + "@monaco-editor/react": "^4.6.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", - "axios": "^1.6.2", + "antd": "^5.15.0", + "axios": "^1.6.8", "clsx": "^2.0.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.294.0", + "lucide-react": "^0.363.0", + "monaco-editor": "^0.47.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.20.1", - "socket.io-client": "^4.7.4", + "react-router-dom": "^6.22.3", + "socket.io-client": "^4.7.5", "tailwind-merge": "^2.1.0", - "zustand": "^4.4.7" + "zustand": "^4.5.2" }, "devDependencies": { "@types/jsonwebtoken": "^9.0.5", @@ -66,6 +69,103 @@ "node": ">=6.0.0" } }, + "node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.23.0.tgz", + "integrity": "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -320,6 +420,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -368,6 +477,18 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -994,6 +1115,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1043,6 +1187,155 @@ "node": ">=14" } }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", + "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.7.tgz", + "integrity": "sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@remix-run/router": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", @@ -1783,6 +2076,71 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd": { + "version": "5.26.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.26.4.tgz", + "integrity": "sha512-e1EnOvEkvvqcQ18dxfzChBJyJACyih13WpNf2OtnP9z2POh/SF0fXL+ynUemT1zfr+p+P1po/tmHXaMc5PMghg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.2.7", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.8", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.51.1", + "rc-tabs": "~15.6.1", + "rc-textarea": "~1.10.0", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.9.2", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -2074,6 +2432,12 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2125,6 +2489,12 @@ "node": ">= 6" } }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2139,6 +2509,15 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2171,7 +2550,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "license": "MIT" }, "node_modules/debug": { @@ -3295,6 +3679,15 @@ "dev": true, "license": "MIT" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3483,9 +3876,9 @@ } }, "node_modules/lucide-react": { - "version": "0.294.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.294.0.tgz", - "integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==", + "version": "0.363.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.363.0.tgz", + "integrity": "sha512-AlsfPCsXQyQx7wwsIgzcKOL9LwC498LIMAo+c0Es5PkHJa33xwmYAkkSoKoJWWWSYQEStqu58/jT4tL2gi32uQ==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" @@ -3571,6 +3964,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/monaco-editor": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz", + "integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4050,6 +4449,612 @@ ], "license": "MIT" }, + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz", + "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", + "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", + "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.51.1", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.51.1.tgz", + "integrity": "sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.6.1.tgz", + "integrity": "sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.0.tgz", + "integrity": "sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz", + "integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.1.tgz", + "integrity": "sha512-DCapO2oyPqmooGhxBuXHM4lFuX+sshQwWqqkuyFA+4rShLe//+GEPVwiDgO+jKtKHtbeYwZoNvetwfHdOf+iUQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -4075,6 +5080,12 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4140,6 +5151,12 @@ "node": ">=8.10.0" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4292,6 +5309,15 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -4422,6 +5448,18 @@ "node": ">=0.10.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4532,6 +5570,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -4696,6 +5740,15 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4709,6 +5762,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", diff --git a/client/package.json b/client/package.json index 718e2fd..46a02ae 100644 --- a/client/package.json +++ b/client/package.json @@ -10,19 +10,22 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { + "@monaco-editor/react": "^4.6.0", + "antd": "^5.15.0", + "axios": "^1.6.8", + "clsx": "^2.0.0", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.363.0", + "monaco-editor": "^0.47.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.20.1", - "socket.io-client": "^4.7.4", - "zustand": "^4.4.7", + "react-router-dom": "^6.22.3", + "socket.io-client": "^4.7.5", + "tailwind-merge": "^2.1.0", + "zustand": "^4.5.2", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", - "@xterm/xterm": "^5.5.0", - "lucide-react": "^0.294.0", - "clsx": "^2.0.0", - "tailwind-merge": "^2.1.0", - "jsonwebtoken": "^9.0.2", - "axios": "^1.6.2" + "@xterm/xterm": "^5.5.0" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/client/src/App.tsx b/client/src/App.tsx index c2b8e0c..d6cf396 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react' import { Routes, Route, Navigate } from 'react-router-dom' +import { ConfigProvider, theme } from 'antd' import { useAuthStore } from '@/stores/authStore' import { useThemeStore } from '@/stores/themeStore' import Layout from '@/components/Layout' @@ -7,6 +8,7 @@ import LoginPage from '@/pages/LoginPage' import HomePage from '@/pages/HomePage' import TerminalPage from '@/pages/TerminalPage' import SettingsPage from '@/pages/SettingsPage' +import FileManagerPage from '@/pages/FileManagerPage' import LoadingSpinner from '@/components/LoadingSpinner' import NotificationContainer from '@/components/NotificationContainer' @@ -42,7 +44,7 @@ const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { function App() { const { verifyToken, setLoading } = useAuthStore() - const { initTheme } = useThemeStore() + const { theme: currentTheme, initTheme } = useThemeStore() useEffect(() => { // 初始化主题 @@ -64,39 +66,49 @@ function App() { }, []) return ( -
- - {/* 公共路由 */} - - - - } - /> + +
+ + {/* 公共路由 */} + + + + } + /> + + {/* 受保护的路由 */} + + + + } /> + } /> + } /> + } /> + } /> + + + + } + /> + - {/* 受保护的路由 */} - - - - } /> - } /> - } /> - } /> - - - - } - /> - - - {/* 全局通知容器 */} - -
+ {/* 全局通知容器 */} + +
+ ) } diff --git a/client/src/components/FileContextMenu.tsx b/client/src/components/FileContextMenu.tsx new file mode 100644 index 0000000..fe52c76 --- /dev/null +++ b/client/src/components/FileContextMenu.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import { Dropdown, Menu } from 'antd' +import type { MenuProps } from 'antd' +import { + EditOutlined, + DeleteOutlined, + CopyOutlined, + ScissorOutlined, + DownloadOutlined, + FileOutlined, + FolderOpenOutlined, + EyeOutlined +} from '@ant-design/icons' +import { FileItem } from '@/types/file' + +interface FileContextMenuProps { + children: React.ReactNode + file: FileItem + selectedFiles: Set + onOpen: (file: FileItem) => void + onRename: (file: FileItem) => void + onDelete: (files: FileItem[]) => void + onDownload: (file: FileItem) => void + onCopy: (files: FileItem[]) => void + onCut: (files: FileItem[]) => void + onView: (file: FileItem) => void +} + +export const FileContextMenu: React.FC = ({ + children, + file, + selectedFiles, + onOpen, + onRename, + onDelete, + onDownload, + onCopy, + onCut, + onView +}) => { + const isSelected = selectedFiles.has(file.path) + const selectedCount = selectedFiles.size + const isMultipleSelected = selectedCount > 1 + + const getSelectedFiles = (): FileItem[] => { + if (isSelected && isMultipleSelected) { + // 如果当前文件被选中且有多个选中项,操作所有选中的文件 + return Array.from(selectedFiles).map(path => ({ ...file, path })) + } else { + // 否则只操作当前文件 + return [file] + } + } + + const menuItems: MenuProps['items'] = [ + // 打开/查看 + { + key: 'open', + label: file.type === 'directory' ? '打开文件夹' : '打开文件', + icon: file.type === 'directory' ? : , + onClick: () => onOpen(file) + }, + + // 查看(仅文件) + ...(file.type === 'file' ? [{ + key: 'view', + label: '预览', + icon: , + onClick: () => onView(file) + }] : []), + + { type: 'divider' as const }, + + // 重命名(仅单个文件) + ...(!isMultipleSelected ? [{ + key: 'rename', + label: '重命名', + icon: , + onClick: () => onRename(file) + }] : []), + + // 复制 + { + key: 'copy', + label: isMultipleSelected ? `复制 ${selectedCount} 项` : '复制', + icon: , + onClick: () => onCopy(getSelectedFiles()) + }, + + // 剪切 + { + key: 'cut', + label: isMultipleSelected ? `剪切 ${selectedCount} 项` : '剪切', + icon: , + onClick: () => onCut(getSelectedFiles()) + }, + + { type: 'divider' as const }, + + // 下载(仅文件) + ...(file.type === 'file' && !isMultipleSelected ? [{ + key: 'download', + label: '下载', + icon: , + onClick: () => onDownload(file) + }] : []), + + // 删除 + { + key: 'delete', + label: isMultipleSelected ? `删除 ${selectedCount} 项` : '删除', + icon: , + danger: true, + onClick: () => onDelete(getSelectedFiles()) + } + ] + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/client/src/components/FileDialogs.tsx b/client/src/components/FileDialogs.tsx new file mode 100644 index 0000000..9666f9f --- /dev/null +++ b/client/src/components/FileDialogs.tsx @@ -0,0 +1,288 @@ +import React, { useState, useEffect } from 'react' +import { Modal, Input, Form, Upload, Button, message } from 'antd' +import { InboxOutlined } from '@ant-design/icons' +import type { UploadProps } from 'antd' + +const { Dragger } = Upload + +interface CreateDialogProps { + visible: boolean + type: 'file' | 'folder' + onConfirm: (name: string) => void + onCancel: () => void +} + +export const CreateDialog: React.FC = ({ + visible, + type, + onConfirm, + onCancel +}) => { + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (visible) { + form.resetFields() + } + }, [visible, form]) + + const handleOk = async () => { + try { + setLoading(true) + const values = await form.validateFields() + onConfirm(values.name) + form.resetFields() + } catch (error) { + // 验证失败 + } finally { + setLoading(false) + } + } + + return ( + +
+ :"/\\|?*]+$/, + message: '名称不能包含特殊字符' + } + ]} + > + + +
+
+ ) +} + +interface RenameDialogProps { + visible: boolean + currentName: string + onConfirm: (newName: string) => void + onCancel: () => void +} + +export const RenameDialog: React.FC = ({ + visible, + currentName, + onConfirm, + onCancel +}) => { + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (visible) { + form.setFieldsValue({ name: currentName }) + } + }, [visible, currentName, form]) + + const handleOk = async () => { + try { + setLoading(true) + const values = await form.validateFields() + onConfirm(values.name) + form.resetFields() + } catch (error) { + // 验证失败 + } finally { + setLoading(false) + } + } + + return ( + +
+ :"/\\|?*]+$/, + message: '名称不能包含特殊字符' + } + ]} + > + + +
+
+ ) +} + +interface UploadDialogProps { + visible: boolean + onConfirm: (files: FileList) => void + onCancel: () => void +} + +export const UploadDialog: React.FC = ({ + visible, + onConfirm, + onCancel +}) => { + const [fileList, setFileList] = useState([]) + const [loading, setLoading] = useState(false) + + const uploadProps: UploadProps = { + name: 'files', + multiple: true, + beforeUpload: (file) => { + setFileList(prev => [...prev, file]) + return false // 阻止自动上传 + }, + onRemove: (file) => { + setFileList(prev => prev.filter(f => f.uid !== file.uid)) + }, + fileList: fileList.map(file => ({ + uid: file.name + file.size, + name: file.name, + status: 'done' as const, + originFileObj: file + })) + } + + const handleOk = async () => { + if (fileList.length === 0) { + message.warning('请选择要上传的文件') + return + } + + setLoading(true) + try { + const files = { + length: fileList.length, + item: (index: number) => fileList[index], + [Symbol.iterator]: function* () { + for (let i = 0; i < this.length; i++) { + yield this.item(i) + } + } + } as FileList + + onConfirm(files) + setFileList([]) + } finally { + setLoading(false) + } + } + + const handleCancel = () => { + setFileList([]) + onCancel() + } + + return ( + +
+ +

+ +

+

+ 点击或拖拽文件到此区域上传 +

+

+ 支持单个或批量上传文件 +

+
+
+
+ ) +} + +interface DeleteConfirmDialogProps { + visible: boolean + fileNames: string[] + onConfirm: () => void + onCancel: () => void +} + +export const DeleteConfirmDialog: React.FC = ({ + visible, + fileNames, + onConfirm, + onCancel +}) => { + const [loading, setLoading] = useState(false) + + const handleOk = async () => { + setLoading(true) + try { + onConfirm() + } finally { + setLoading(false) + } + } + + return ( + +
+

+ 确定要删除以下{fileNames.length}个项目吗?此操作不可撤销。 +

+
+ {fileNames.map((name, index) => ( +
+ {name} +
+ ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/client/src/components/FileGridItem.tsx b/client/src/components/FileGridItem.tsx new file mode 100644 index 0000000..9444d3a --- /dev/null +++ b/client/src/components/FileGridItem.tsx @@ -0,0 +1,155 @@ +import React from 'react' +import { FileItem } from '@/types/file' +import { + FolderOutlined, + FileTextOutlined, + FileImageOutlined, + FileZipOutlined, + VideoCameraOutlined, + AudioOutlined, + CodeOutlined, + FileOutlined +} from '@ant-design/icons' +import { formatFileSize, formatDate } from '@/utils/format' + +interface FileGridItemProps { + file: FileItem + isSelected: boolean + onClick: (file: FileItem, event: React.MouseEvent) => void + onDoubleClick: (file: FileItem) => void +} + +// 根据文件扩展名获取图标 +const getFileIcon = (fileName: string, type: string) => { + if (type === 'directory') { + return + } + + const ext = fileName.split('.').pop()?.toLowerCase() + + switch (ext) { + case 'txt': + case 'md': + case 'doc': + case 'docx': + return + + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': + case 'bmp': + case 'svg': + case 'webp': + return + + case 'zip': + case 'rar': + case '7z': + case 'tar': + case 'gz': + return + + case 'mp4': + case 'avi': + case 'mkv': + case 'mov': + case 'wmv': + case 'flv': + return + + case 'mp3': + case 'wav': + case 'flac': + case 'aac': + case 'ogg': + return + + case 'js': + case 'ts': + case 'jsx': + case 'tsx': + case 'html': + case 'css': + case 'scss': + case 'json': + case 'xml': + case 'py': + case 'java': + case 'cpp': + case 'c': + case 'php': + case 'go': + case 'rs': + return + + default: + return + } +} + +export const FileGridItem: React.FC = ({ + file, + isSelected, + onClick, + onDoubleClick +}) => { + const handleClick = (event: React.MouseEvent) => { + onClick(file, event) + } + + const handleDoubleClick = () => { + onDoubleClick(file) + } + + return ( +
+ {/* 选中状态指示器 */} + {isSelected && ( +
+
+
+ )} + + {/* 文件图标 */} +
+
+ {getFileIcon(file.name, file.type)} +
+ + {/* 文件名 */} +
+
+ {file.name} +
+ + {/* 文件信息 */} +
+ {file.type === 'file' && ( +
{formatFileSize(file.size)}
+ )} +
{formatDate(file.modified)}
+
+
+
+ + {/* 悬停效果 */} + +
+ ) +} \ No newline at end of file diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index c6260d8..8ca665b 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -12,7 +12,8 @@ import { Sun, Moon, User, - Gamepad2 + Gamepad2, + FolderOpen } from 'lucide-react' interface LayoutProps { @@ -28,6 +29,7 @@ const Layout: React.FC = ({ children }) => { const navigation = [ { name: '首页', href: '/', icon: Home }, { name: '终端', href: '/terminal', icon: Terminal }, + { name: '文件管理', href: '/files', icon: FolderOpen }, { name: '设置', href: '/settings', icon: Settings }, ] diff --git a/client/src/components/MonacoEditor.tsx b/client/src/components/MonacoEditor.tsx new file mode 100644 index 0000000..492bbd0 --- /dev/null +++ b/client/src/components/MonacoEditor.tsx @@ -0,0 +1,161 @@ +import React, { useRef, useEffect } from 'react' +import { Editor } from '@monaco-editor/react' +import { useThemeStore } from '@/stores/themeStore' +import { getFileExtension } from '@/utils/format' +import type { editor } from 'monaco-editor' + +interface MonacoEditorProps { + value: string + onChange: (value: string) => void + fileName?: string + readOnly?: boolean + height?: string | number + onSave?: () => void +} + +// 根据文件扩展名获取语言 +const getLanguageFromFileName = (fileName: string): string => { + const ext = getFileExtension(fileName) + + const languageMap: Record = { + 'js': 'javascript', + 'jsx': 'javascript', + 'ts': 'typescript', + 'tsx': 'typescript', + 'html': 'html', + 'css': 'css', + 'scss': 'scss', + 'less': 'less', + 'json': 'json', + 'xml': 'xml', + 'md': 'markdown', + 'py': 'python', + 'java': 'java', + 'cpp': 'cpp', + 'c': 'c', + 'h': 'c', + 'php': 'php', + 'go': 'go', + 'rs': 'rust', + 'sql': 'sql', + 'yml': 'yaml', + 'yaml': 'yaml', + 'sh': 'shell', + 'bat': 'bat', + 'ps1': 'powershell', + 'vue': 'html', + 'svelte': 'html' + } + + return languageMap[ext] || 'plaintext' +} + +export const MonacoEditor: React.FC = ({ + value, + onChange, + fileName = '', + readOnly = false, + height = '100%', + onSave +}) => { + const { theme } = useThemeStore() + const editorRef = useRef(null) + + const language = getLanguageFromFileName(fileName) + const monacoTheme = theme === 'dark' ? 'vs-dark' : 'vs' + + const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor) => { + editorRef.current = editor + + // 添加保存快捷键 + editor.addCommand( + // Ctrl+S 或 Cmd+S + 2048 | 49, // monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS + () => { + onSave?.() + } + ) + + // 设置编辑器选项 + editor.updateOptions({ + fontSize: 14, + fontFamily: 'Consolas, "Courier New", monospace', + lineHeight: 20, + minimap: { enabled: true }, + scrollBeyondLastLine: false, + automaticLayout: true, + wordWrap: 'on', + tabSize: 2, + insertSpaces: true, + renderWhitespace: 'selection', + renderControlCharacters: true, + smoothScrolling: true, + cursorBlinking: 'smooth', + cursorSmoothCaretAnimation: 'on' + }) + } + + const handleEditorChange = (value: string | undefined) => { + onChange(value || '') + } + + // 当主题改变时更新编辑器主题 + useEffect(() => { + if (editorRef.current) { + // 使用 monaco.editor.setTheme 来动态切换主题 + import('monaco-editor').then(monaco => { + monaco.editor.setTheme(monacoTheme) + }) + } + }, [monacoTheme]) + + return ( +
+ +
+
+ } + /> + + ) +} \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index a23e060..b50ee81 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -2,6 +2,98 @@ @tailwind components; @tailwind utilities; +/* 文件管理相关样式 */ +.file-context-menu .ant-dropdown-menu { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 8px; + padding: 4px; +} + +.dark .file-context-menu .ant-dropdown-menu { + background: rgba(31, 41, 55, 0.95); + border: 1px solid rgba(75, 85, 99, 0.3); +} + +.file-context-menu .ant-dropdown-menu-item { + border-radius: 4px; + margin: 1px 0; +} + +.file-context-menu .ant-dropdown-menu-item:hover { + background: rgba(59, 130, 246, 0.1); +} + +.file-context-menu .ant-dropdown-menu-item-danger:hover { + background: rgba(239, 68, 68, 0.1); +} + +/* 文件网格项悬停效果 */ +.file-grid-item:hover { + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +/* Monaco Editor 容器样式 */ +.monaco-editor-container { + border: 1px solid #e5e7eb; + border-radius: 8px; + overflow: hidden; +} + +.dark .monaco-editor-container { + border-color: #374151; +} + +/* Ant Design 组件自定义样式 */ +.ant-modal { + backdrop-filter: blur(4px); +} + +.ant-modal-content { + border-radius: 12px; + overflow: hidden; +} + +.ant-tabs-content-holder { + height: calc(100% - 46px); +} + +.ant-tabs-tabpane { + height: 100%; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.5); +} + +.dark ::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.1); +} + +.dark ::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); +} + +.dark ::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.5); +} + /* 游戏风格自定义样式 */ @layer base { * { diff --git a/client/src/pages/FileManagerPage.tsx b/client/src/pages/FileManagerPage.tsx new file mode 100644 index 0000000..ea36d6c --- /dev/null +++ b/client/src/pages/FileManagerPage.tsx @@ -0,0 +1,605 @@ +import React, { useEffect, useState, useCallback } from 'react' +import { + Button, + Input, + Breadcrumb, + Spin, + Empty, + message, + Tooltip, + Space, + Tabs, + Card, + Modal +} from 'antd' +import { + HomeOutlined, + FolderOutlined, + PlusOutlined, + UploadOutlined, + DeleteOutlined, + ReloadOutlined, + SearchOutlined, + LeftOutlined, + RightOutlined, + FileTextOutlined, + SaveOutlined, + CloseOutlined +} from '@ant-design/icons' +import { useFileStore } from '@/stores/fileStore' +import { useNotificationStore } from '@/stores/notificationStore' +import { FileGridItem } from '@/components/FileGridItem' +import { FileContextMenu } from '@/components/FileContextMenu' +import { + CreateDialog, + RenameDialog, + UploadDialog, + DeleteConfirmDialog +} from '@/components/FileDialogs' +import { MonacoEditor } from '@/components/MonacoEditor' +import { FileItem } from '@/types/file' +import { fileApiClient } from '@/utils/fileApi' +import { isTextFile } from '@/utils/format' +import { normalizePath, getDirectoryPath, getBasename } from '@/utils/pathUtils' + +const { TabPane } = Tabs + +const FileManagerPage: React.FC = () => { + const { + currentPath, + files, + selectedFiles, + loading, + error, + openFiles, + activeFile, + setCurrentPath, + loadFiles, + selectFile, + unselectFile, + clearSelection, + toggleFileSelection, + createDirectory, + deleteSelectedFiles, + renameFile, + uploadFiles, + openFile, + closeFile, + saveFile, + setActiveFile, + setError + } = useFileStore() + + const { addNotification } = useNotificationStore() + + // 对话框状态 + const [createDialog, setCreateDialog] = useState<{ + visible: boolean + type: 'file' | 'folder' + }>({ visible: false, type: 'folder' }) + + const [renameDialog, setRenameDialog] = useState<{ + visible: boolean + file: FileItem | null + }>({ visible: false, file: null }) + + const [uploadDialog, setUploadDialog] = useState(false) + const [deleteDialog, setDeleteDialog] = useState(false) + + // 路径输入 + const [pathInput, setPathInput] = useState('') + const [isEditingPath, setIsEditingPath] = useState(false) + + // 搜索 + const [searchQuery, setSearchQuery] = useState('') + + // 历史记录 + const [history, setHistory] = useState([]) + const [historyIndex, setHistoryIndex] = useState(-1) + + // 编辑器模态框 + const [editorModalVisible, setEditorModalVisible] = useState(false) + + // 初始化 + useEffect(() => { + loadFiles() + }, []) + + // 错误处理 + useEffect(() => { + if (error) { + addNotification({ + type: 'error', + title: '操作失败', + message: error + }) + setError(null) + } + }, [error, addNotification, setError]) + + // 更新路径输入 + useEffect(() => { + setPathInput(currentPath) + }, [currentPath]) + + // 导航到指定路径 + const navigateToPath = useCallback((newPath: string) => { + const normalizedPath = normalizePath(newPath) + + // 更新历史记录 + const newHistory = history.slice(0, historyIndex + 1) + newHistory.push(normalizedPath) + setHistory(newHistory) + setHistoryIndex(newHistory.length - 1) + + setCurrentPath(normalizedPath) + }, [history, historyIndex, setCurrentPath]) + + // 后退 + const goBack = () => { + if (historyIndex > 0) { + const newIndex = historyIndex - 1 + setHistoryIndex(newIndex) + setCurrentPath(history[newIndex]) + } + } + + // 前进 + const goForward = () => { + if (historyIndex < history.length - 1) { + const newIndex = historyIndex + 1 + setHistoryIndex(newIndex) + setCurrentPath(history[newIndex]) + } + } + + // 上级目录 + const goUp = () => { + const parentPath = getDirectoryPath(currentPath) + if (parentPath !== currentPath) { + navigateToPath(parentPath) + } + } + + // 处理路径输入 + const handlePathSubmit = () => { + if (pathInput.trim()) { + navigateToPath(pathInput.trim()) + } + setIsEditingPath(false) + } + + // 文件点击处理 + const handleFileClick = (file: FileItem, event: React.MouseEvent) => { + if (event.ctrlKey || event.metaKey) { + // Ctrl/Cmd + 点击:多选 + toggleFileSelection(file.path) + } else if (event.shiftKey && selectedFiles.size > 0) { + // Shift + 点击:范围选择 + const lastSelected = Array.from(selectedFiles)[selectedFiles.size - 1] + const lastIndex = files.findIndex(f => f.path === lastSelected) + const currentIndex = files.findIndex(f => f.path === file.path) + + if (lastIndex !== -1 && currentIndex !== -1) { + const start = Math.min(lastIndex, currentIndex) + const end = Math.max(lastIndex, currentIndex) + const rangeFiles = files.slice(start, end + 1).map(f => f.path) + + clearSelection() + rangeFiles.forEach(path => selectFile(path)) + } + } else { + // 普通点击:单选 + clearSelection() + selectFile(file.path) + } + } + + // 文件双击处理 + const handleFileDoubleClick = (file: FileItem) => { + if (file.type === 'directory') { + navigateToPath(file.path) + } else if (isTextFile(file.name)) { + openFile(file.path) + setEditorModalVisible(true) + } else { + // 非文本文件,提示下载 + message.info('该文件类型不支持在线编辑,请下载查看') + } + } + + // 右键菜单处理 + const handleContextMenuOpen = (file: FileItem) => { + if (file.type === 'directory') { + navigateToPath(file.path) + } else { + openFile(file.path) + setEditorModalVisible(true) + } + } + + const handleContextMenuRename = (file: FileItem) => { + setRenameDialog({ visible: true, file }) + } + + const handleContextMenuDelete = (files: FileItem[]) => { + // 选中要删除的文件 + clearSelection() + files.forEach(file => selectFile(file.path)) + setDeleteDialog(true) + } + + const handleContextMenuDownload = (file: FileItem) => { + fileApiClient.downloadFile(file.path) + addNotification({ + type: 'success', + title: '下载开始', + message: `正在下载 ${file.name}` + }) + } + + const handleContextMenuCopy = (files: FileItem[]) => { + // TODO: 实现复制功能 + message.info('复制功能开发中') + } + + const handleContextMenuCut = (files: FileItem[]) => { + // TODO: 实现剪切功能 + message.info('剪切功能开发中') + } + + const handleContextMenuView = (file: FileItem) => { + if (isTextFile(file.name)) { + openFile(file.path) + setEditorModalVisible(true) + } else { + message.info('该文件类型不支持预览') + } + } + + // 对话框处理 + const handleCreateConfirm = async (name: string) => { + if (createDialog.type === 'folder') { + const success = await createDirectory(name) + if (success) { + addNotification({ + type: 'success', + title: '创建成功', + message: `文件夹 "${name}" 创建成功` + }) + } + } else { + // TODO: 创建文件 + message.info('创建文件功能开发中') + } + setCreateDialog({ visible: false, type: 'folder' }) + } + + const handleRenameConfirm = async (newName: string) => { + if (renameDialog.file) { + const success = await renameFile(renameDialog.file.path, newName) + if (success) { + addNotification({ + type: 'success', + title: '重命名成功', + message: `"${renameDialog.file.name}" 已重命名为 "${newName}"` + }) + } + } + setRenameDialog({ visible: false, file: null }) + } + + const handleUploadConfirm = async (files: FileList) => { + const success = await uploadFiles(files) + if (success) { + addNotification({ + type: 'success', + title: '上传成功', + message: `成功上传 ${files.length} 个文件` + }) + } + setUploadDialog(false) + } + + const handleDeleteConfirm = async () => { + const success = await deleteSelectedFiles() + if (success) { + addNotification({ + type: 'success', + title: '删除成功', + message: `成功删除 ${selectedFiles.size} 个项目` + }) + } + setDeleteDialog(false) + } + + // 编辑器相关 + const handleEditorChange = (path: string, content: string) => { + const newOpenFiles = new Map(openFiles) + newOpenFiles.set(path, content) + // 这里需要更新store中的openFiles + } + + const handleSaveFile = async () => { + if (activeFile && openFiles.has(activeFile)) { + const content = openFiles.get(activeFile)! + const success = await saveFile(activeFile, content) + if (success) { + addNotification({ + type: 'success', + title: '保存成功', + message: `文件已保存` + }) + } + } + } + + // 生成面包屑 + const generateBreadcrumbs = () => { + const parts = currentPath.split('/').filter(Boolean) + const items = [ + { + title: ( + navigateToPath('/')}> + + 根目录 + + ) + } + ] + + let currentBreadcrumbPath = '' + parts.forEach((part, index) => { + currentBreadcrumbPath += '/' + part + const breadcrumbPath = currentBreadcrumbPath + + items.push({ + title: ( + navigateToPath(breadcrumbPath)} + > + {part} + + ) + }) + }) + + return items + } + + // 过滤文件 + const filteredFiles = files.filter(file => + file.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + + return ( +
+ {/* 工具栏 */} +
+
+ {/* 导航按钮 */} + + +
+ +
+ {/* 搜索 */} + } + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + className="w-64" + /> + + {/* 操作按钮 */} + + + + + + + + {selectedFiles.size > 0 && ( + + + + )} + +
+
+ + {/* 路径栏 */} +
+ {isEditingPath ? ( + setPathInput(e.target.value)} + onPressEnter={handlePathSubmit} + onBlur={handlePathSubmit} + autoFocus + /> + ) : ( +
setIsEditingPath(true)} className="cursor-pointer"> + +
+ )} +
+ + {/* 主内容区 */} +
+ {loading ? ( +
+ +
+ ) : filteredFiles.length === 0 ? ( + + ) : ( +
+ {filteredFiles.map((file) => ( + + + + ))} +
+ )} +
+ + {/* 对话框 */} + setCreateDialog({ visible: false, type: 'folder' })} + /> + + setRenameDialog({ visible: false, file: null })} + /> + + setUploadDialog(false)} + /> + + path.split('/').pop() || '')} + onConfirm={handleDeleteConfirm} + onCancel={() => setDeleteDialog(false)} + /> + + {/* 编辑器模态框 */} + setEditorModalVisible(false)} + width="90%" + style={{ top: 20 }} + bodyStyle={{ height: '80vh', padding: 0 }} + footer={[ + , + + ]} + > + {openFiles.size > 0 && ( + { + if (action === 'remove' && typeof targetKey === 'string') { + closeFile(targetKey) + if (openFiles.size === 1) { + setEditorModalVisible(false) + } + } + }} + className="h-full" + > + {Array.from(openFiles.entries()).map(([filePath, content]) => ( + + + {getBasename(filePath)} + + } + closable + > +
+ handleEditorChange(filePath, value)} + fileName={getBasename(filePath)} + onSave={handleSaveFile} + /> +
+
+ ))} +
+ )} +
+
+ ) +} + +export default FileManagerPage \ No newline at end of file diff --git a/client/src/stores/fileStore.ts b/client/src/stores/fileStore.ts new file mode 100644 index 0000000..7777f7e --- /dev/null +++ b/client/src/stores/fileStore.ts @@ -0,0 +1,253 @@ +import { create } from 'zustand' +import { FileItem } from '@/types/file' +import { fileApiClient } from '@/utils/fileApi' + +interface FileStore { + // 状态 + currentPath: string + files: FileItem[] + selectedFiles: Set + loading: boolean + error: string | null + + // 编辑器相关 + openFiles: Map // path -> content + activeFile: string | null + + // 操作方法 + setCurrentPath: (path: string) => void + loadFiles: (path?: string) => Promise + selectFile: (path: string) => void + selectMultipleFiles: (paths: string[]) => void + unselectFile: (path: string) => void + clearSelection: () => void + toggleFileSelection: (path: string) => void + + // 文件操作 + createDirectory: (name: string) => Promise + deleteSelectedFiles: () => Promise + renameFile: (oldPath: string, newName: string) => Promise + uploadFiles: (files: FileList) => Promise + + // 编辑器操作 + openFile: (path: string) => Promise + closeFile: (path: string) => void + saveFile: (path: string, content: string) => Promise + setActiveFile: (path: string | null) => void + + // 工具方法 + setLoading: (loading: boolean) => void + setError: (error: string | null) => void +} + +export const useFileStore = create((set, get) => ({ + // 初始状态 + currentPath: '/', + files: [], + selectedFiles: new Set(), + loading: false, + error: null, + openFiles: new Map(), + activeFile: null, + + // 设置当前路径 + setCurrentPath: (path: string) => { + set({ currentPath: path }) + get().loadFiles(path) + }, + + // 加载文件列表 + loadFiles: async (path?: string) => { + const targetPath = path || get().currentPath + set({ loading: true, error: null }) + + try { + const files = await fileApiClient.listDirectory(targetPath) + // 排序:文件夹优先,然后按名称排序 + const sortedFiles = files.sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'directory' ? -1 : 1 + } + return a.name.localeCompare(b.name) + }) + + set({ + files: sortedFiles, + currentPath: targetPath, + loading: false, + selectedFiles: new Set() // 清空选择 + }) + } catch (error: any) { + set({ + error: error.message || '加载文件列表失败', + loading: false + }) + } + }, + + // 选择文件 + selectFile: (path: string) => { + const selectedFiles = new Set(get().selectedFiles) + selectedFiles.add(path) + set({ selectedFiles }) + }, + + // 选择多个文件 + selectMultipleFiles: (paths: string[]) => { + const selectedFiles = new Set(get().selectedFiles) + paths.forEach(path => selectedFiles.add(path)) + set({ selectedFiles }) + }, + + // 取消选择文件 + unselectFile: (path: string) => { + const selectedFiles = new Set(get().selectedFiles) + selectedFiles.delete(path) + set({ selectedFiles }) + }, + + // 清空选择 + clearSelection: () => { + set({ selectedFiles: new Set() }) + }, + + // 切换文件选择状态 + toggleFileSelection: (path: string) => { + const selectedFiles = new Set(get().selectedFiles) + if (selectedFiles.has(path)) { + selectedFiles.delete(path) + } else { + selectedFiles.add(path) + } + set({ selectedFiles }) + }, + + // 创建目录 + createDirectory: async (name: string) => { + const { currentPath } = get() + const newPath = `${currentPath}/${name}`.replace(/\/+/g, '/') + + try { + await fileApiClient.createDirectory(newPath) + await get().loadFiles() + return true + } catch (error: any) { + set({ error: error.message || '创建目录失败' }) + return false + } + }, + + // 删除选中的文件 + deleteSelectedFiles: async () => { + const { selectedFiles } = get() + if (selectedFiles.size === 0) return false + + try { + await fileApiClient.deleteItems(Array.from(selectedFiles)) + await get().loadFiles() + set({ selectedFiles: new Set() }) + return true + } catch (error: any) { + set({ error: error.message || '删除文件失败' }) + return false + } + }, + + // 重命名文件 + renameFile: async (oldPath: string, newName: string) => { + const newPath = oldPath.replace(/[^/]*$/, newName) + + try { + await fileApiClient.renameItem(oldPath, newPath) + await get().loadFiles() + return true + } catch (error: any) { + set({ error: error.message || '重命名失败' }) + return false + } + }, + + // 上传文件 + uploadFiles: async (files: FileList) => { + const { currentPath } = get() + + try { + await fileApiClient.uploadFiles(currentPath, files) + await get().loadFiles() + return true + } catch (error: any) { + set({ error: error.message || '上传文件失败' }) + return false + } + }, + + // 打开文件 + openFile: async (path: string) => { + const { openFiles } = get() + + if (openFiles.has(path)) { + set({ activeFile: path }) + return + } + + try { + const fileContent = await fileApiClient.readFile(path) + const newOpenFiles = new Map(openFiles) + newOpenFiles.set(path, fileContent.content) + + set({ + openFiles: newOpenFiles, + activeFile: path + }) + } catch (error: any) { + set({ error: error.message || '打开文件失败' }) + } + }, + + // 关闭文件 + closeFile: (path: string) => { + const { openFiles, activeFile } = get() + const newOpenFiles = new Map(openFiles) + newOpenFiles.delete(path) + + const newActiveFile = activeFile === path ? + (newOpenFiles.size > 0 ? Array.from(newOpenFiles.keys())[0] : null) : + activeFile + + set({ + openFiles: newOpenFiles, + activeFile: newActiveFile + }) + }, + + // 保存文件 + saveFile: async (path: string, content: string) => { + try { + await fileApiClient.saveFile(path, content) + const { openFiles } = get() + const newOpenFiles = new Map(openFiles) + newOpenFiles.set(path, content) + + set({ openFiles: newOpenFiles }) + return true + } catch (error: any) { + set({ error: error.message || '保存文件失败' }) + return false + } + }, + + // 设置活动文件 + setActiveFile: (path: string | null) => { + set({ activeFile: path }) + }, + + // 设置加载状态 + setLoading: (loading: boolean) => { + set({ loading }) + }, + + // 设置错误信息 + setError: (error: string | null) => { + set({ error }) + } +})) \ No newline at end of file diff --git a/client/src/types/file.ts b/client/src/types/file.ts new file mode 100644 index 0000000..b4278fb --- /dev/null +++ b/client/src/types/file.ts @@ -0,0 +1,33 @@ +export interface FileItem { + name: string + path: string + type: 'file' | 'directory' + size: number + modified: string +} + +export interface FileOperationResult { + status: 'success' | 'error' + message: string + data?: any +} + +export interface FileUploadProgress { + fileName: string + progress: number + status: 'uploading' | 'completed' | 'error' +} + +export interface FileSearchResult { + status: 'success' | 'error' + results: FileItem[] + total_found: number + truncated: boolean +} + +export interface FileContent { + content: string + encoding: string + size: number + modified: string +} \ No newline at end of file diff --git a/client/src/types/index.ts b/client/src/types/index.ts index ef80bc7..a592f47 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -197,4 +197,7 @@ export interface SettingsState { resetSettings: () => void loading: boolean error: string | null -} \ No newline at end of file +} + +// 文件管理相关类型 +export * from './file' \ No newline at end of file diff --git a/client/src/utils/fileApi.ts b/client/src/utils/fileApi.ts new file mode 100644 index 0000000..1599560 --- /dev/null +++ b/client/src/utils/fileApi.ts @@ -0,0 +1,125 @@ +import axios from 'axios' +import { FileItem, FileOperationResult, FileSearchResult, FileContent } from '@/types/file' + +const API_BASE = '/api/files' + +export class FileApiClient { + // 获取目录内容 + async listDirectory(path: string = '/'): Promise { + const response = await axios.get(`${API_BASE}/list`, { + params: { path } + }) + return response.data.data + } + + // 读取文件内容 + async readFile(path: string, encoding: string = 'utf-8'): Promise { + const response = await axios.get(`${API_BASE}/read`, { + params: { path, encoding } + }) + return response.data.data + } + + // 保存文件内容 + async saveFile(path: string, content: string, encoding: string = 'utf-8'): Promise { + const response = await axios.post(`${API_BASE}/save`, { + path, + content, + encoding + }) + return response.data + } + + // 创建目录 + async createDirectory(path: string): Promise { + const response = await axios.post(`${API_BASE}/mkdir`, { + path + }) + return response.data + } + + // 删除文件或目录 + async deleteItems(paths: string[]): Promise { + const response = await axios.delete(`${API_BASE}/delete`, { + data: { paths } + }) + return response.data + } + + // 重命名文件或目录 + async renameItem(oldPath: string, newPath: string): Promise { + const response = await axios.post(`${API_BASE}/rename`, { + oldPath, + newPath + }) + return response.data + } + + // 复制文件或目录 + async copyItem(sourcePath: string, targetPath: string): Promise { + const response = await axios.post(`${API_BASE}/copy`, { + sourcePath, + targetPath + }) + return response.data + } + + // 移动文件或目录 + async moveItem(sourcePath: string, targetPath: string): Promise { + const response = await axios.post(`${API_BASE}/move`, { + sourcePath, + targetPath + }) + return response.data + } + + // 搜索文件 + async searchFiles( + searchPath: string, + query: string, + type: string = 'all', + caseSensitive: boolean = false, + maxResults: number = 100 + ): Promise { + const response = await axios.get(`${API_BASE}/search`, { + params: { + path: searchPath, + query, + type, + case_sensitive: caseSensitive, + max_results: maxResults + } + }) + return response.data + } + + // 下载文件 + downloadFile(path: string): void { + const url = `${API_BASE}/download?path=${encodeURIComponent(path)}` + const link = document.createElement('a') + link.href = url + link.download = '' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + + // 上传文件 + async uploadFiles(targetPath: string, files: FileList): Promise { + const formData = new FormData() + formData.append('targetPath', targetPath) + + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]) + } + + const response = await axios.post(`${API_BASE}/upload`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + return response.data + } +} + +export const fileApiClient = new FileApiClient() \ No newline at end of file diff --git a/client/src/utils/format.ts b/client/src/utils/format.ts new file mode 100644 index 0000000..d33ca0b --- /dev/null +++ b/client/src/utils/format.ts @@ -0,0 +1,81 @@ +// 格式化文件大小 +export const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +// 格式化日期 +export const formatDate = (dateString: string): string => { + const date = new Date(dateString) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + + if (diffDays === 0) { + // 今天 + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }) + } else if (diffDays === 1) { + // 昨天 + return '昨天 ' + date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }) + } else if (diffDays < 7) { + // 一周内 + return diffDays + '天前' + } else { + // 超过一周 + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + } +} + +// 获取文件扩展名 +export const getFileExtension = (fileName: string): string => { + return fileName.split('.').pop()?.toLowerCase() || '' +} + +// 判断是否为图片文件 +export const isImageFile = (fileName: string): boolean => { + const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'] + return imageExts.includes(getFileExtension(fileName)) +} + +// 判断是否为文本文件 +export const isTextFile = (fileName: string): boolean => { + const textExts = [ + 'txt', 'md', 'json', 'xml', 'html', 'css', 'js', 'ts', 'jsx', 'tsx', + 'py', 'java', 'cpp', 'c', 'h', 'php', 'go', 'rs', 'sql', 'yml', 'yaml', + 'ini', 'conf', 'log', 'csv', 'scss', 'less', 'vue', 'svelte' + ] + return textExts.includes(getFileExtension(fileName)) +} + +// 判断是否为视频文件 +export const isVideoFile = (fileName: string): boolean => { + const videoExts = ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm', 'm4v'] + return videoExts.includes(getFileExtension(fileName)) +} + +// 判断是否为音频文件 +export const isAudioFile = (fileName: string): boolean => { + const audioExts = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'wma'] + return audioExts.includes(getFileExtension(fileName)) +} + +// 判断是否为压缩文件 +export const isArchiveFile = (fileName: string): boolean => { + const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'] + return archiveExts.includes(getFileExtension(fileName)) +} \ No newline at end of file diff --git a/client/src/utils/monaco.ts b/client/src/utils/monaco.ts new file mode 100644 index 0000000..84b3488 --- /dev/null +++ b/client/src/utils/monaco.ts @@ -0,0 +1,8 @@ +import loader from '@monaco-editor/loader' +import * as monaco from 'monaco-editor' + +// 配置@monaco-editor/react使用本地的monaco-editor包而不是CDN +loader.config({ monaco }) + +export default loader +export { monaco } \ No newline at end of file diff --git a/client/src/utils/pathUtils.ts b/client/src/utils/pathUtils.ts new file mode 100644 index 0000000..dd6ebcb --- /dev/null +++ b/client/src/utils/pathUtils.ts @@ -0,0 +1,168 @@ +/** + * 浏览器兼容的路径处理工具函数 + * 替代Node.js的path模块在浏览器环境中的使用 + */ + +/** + * 标准化路径,类似于path.normalize + * @param pathStr 要标准化的路径 + * @returns 标准化后的路径 + */ +export function normalizePath(pathStr: string): string { + if (!pathStr) return '/' + + // 将反斜杠转换为正斜杠 + let normalized = pathStr.replace(/\\/g, '/') + + // 确保以/开头 + if (!normalized.startsWith('/')) { + normalized = '/' + normalized + } + + // 处理多个连续的斜杠 + normalized = normalized.replace(/\/+/g, '/') + + // 处理 . 和 .. 路径段 + const parts = normalized.split('/').filter(part => part !== '') + const stack: string[] = [] + + for (const part of parts) { + if (part === '..') { + if (stack.length > 0) { + stack.pop() + } + } else if (part !== '.') { + stack.push(part) + } + } + + // 重新构建路径 + const result = '/' + stack.join('/') + return result === '/' ? '/' : result +} + +/** + * 获取路径的目录部分,类似于path.dirname + * @param pathStr 文件路径 + * @returns 目录路径 + */ +export function getDirectoryPath(pathStr: string): string { + if (!pathStr || pathStr === '/') return '/' + + const normalized = normalizePath(pathStr) + const lastSlashIndex = normalized.lastIndexOf('/') + + if (lastSlashIndex === 0) { + return '/' + } + + return normalized.substring(0, lastSlashIndex) +} + +/** + * 连接路径,类似于path.join + * @param paths 要连接的路径段 + * @returns 连接后的路径 + */ +export function joinPaths(...paths: string[]): string { + if (paths.length === 0) return '/' + + const joined = paths + .filter(path => path && path.length > 0) + .join('/') + + return normalizePath(joined) +} + +/** + * 获取文件名,类似于path.basename + * @param pathStr 文件路径 + * @param ext 可选的扩展名,如果提供则会从结果中移除 + * @returns 文件名 + */ +export function getBasename(pathStr: string, ext?: string): string { + if (!pathStr) return '' + + const normalized = normalizePath(pathStr) + const parts = normalized.split('/') + let basename = parts[parts.length - 1] || '' + + if (ext && basename.endsWith(ext)) { + basename = basename.substring(0, basename.length - ext.length) + } + + return basename +} + +/** + * 获取文件扩展名,类似于path.extname + * @param pathStr 文件路径 + * @returns 文件扩展名(包含点号) + */ +export function getExtension(pathStr: string): string { + if (!pathStr) return '' + + const basename = getBasename(pathStr) + const lastDotIndex = basename.lastIndexOf('.') + + if (lastDotIndex === -1 || lastDotIndex === 0) { + return '' + } + + return basename.substring(lastDotIndex) +} + +/** + * 检查路径是否为绝对路径 + * @param pathStr 要检查的路径 + * @returns 是否为绝对路径 + */ +export function isAbsolute(pathStr: string): boolean { + return pathStr && pathStr.startsWith('/') +} + +/** + * 获取相对路径 + * @param from 起始路径 + * @param to 目标路径 + * @returns 相对路径 + */ +export function getRelativePath(from: string, to: string): string { + const fromNormalized = normalizePath(from) + const toNormalized = normalizePath(to) + + if (fromNormalized === toNormalized) { + return '.' + } + + const fromParts = fromNormalized.split('/').filter(part => part !== '') + const toParts = toNormalized.split('/').filter(part => part !== '') + + // 找到公共前缀 + let commonLength = 0 + const minLength = Math.min(fromParts.length, toParts.length) + + for (let i = 0; i < minLength; i++) { + if (fromParts[i] === toParts[i]) { + commonLength++ + } else { + break + } + } + + // 构建相对路径 + const upLevels = fromParts.length - commonLength + const downParts = toParts.slice(commonLength) + + const relativeParts: string[] = [] + + // 添加向上的路径 + for (let i = 0; i < upLevels; i++) { + relativeParts.push('..') + } + + // 添加向下的路径 + relativeParts.push(...downParts) + + return relativeParts.length === 0 ? '.' : relativeParts.join('/') +} \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts index f7858dd..890b1b2 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -23,6 +23,10 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + define: { + global: 'globalThis', + 'process.env': {}, + }, server: { port: clientPort, proxy: { diff --git a/install-deps.md b/install-deps.md new file mode 100644 index 0000000..e110d19 --- /dev/null +++ b/install-deps.md @@ -0,0 +1,13 @@ +# 安装文件管理功能所需依赖 + +请在client目录下运行以下命令安装依赖: + +```bash +cd client +npm install antd @monaco-editor/react monaco-editor +``` + +这将安装: +- antd: Ant Design UI组件库 +- @monaco-editor/react: Monaco Editor的React封装 +- monaco-editor: VS Code编辑器内核 \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index b045bbd..0e817ae 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -19,6 +19,7 @@ import { setupGameRoutes } from './routes/games.js' import { setupSystemRoutes } from './routes/system.js' import { setupAuthRoutes } from './routes/auth.js' import { setAuthManager } from './middleware/auth.js' +import filesRouter from './routes/files.js' // 获取当前文件目录 const __filename = fileURLToPath(import.meta.url) @@ -229,6 +230,7 @@ async function startServer() { app.use('/api/terminal', setupTerminalRoutes(terminalManager)) app.use('/api/game', setupGameRoutes(gameManager)) app.use('/api/system', setupSystemRoutes(systemManager)) + app.use('/api/files', filesRouter) // 前端路由处理(SPA支持) app.get('*', (req, res) => { diff --git a/报错.txt b/报错.txt index 8bcb24a..e69de29 100644 --- a/报错.txt +++ b/报错.txt @@ -1,76 +0,0 @@ -20:35:24 [vite] Pre-transform error: D:\Cursor项目\GSM3\client\src\pages\TerminalPage.tsx: Unexpected token, expected "," (703:6) - - 701 | - 702 | -> 703 | {/* 右侧终端显示区域 */} - | ^ - 704 |
- 705 | {sessions.length === 0 ? ( - 706 |
-20:35:24 [vite] Internal server error: D:\Cursor项目\GSM3\client\src\pages\TerminalPage.tsx: Unexpected token, expected "," (703:6) - - 701 |
- 702 | -> 703 | {/* 右侧终端显示区域 */} - | ^ - 704 |
- 705 | {sessions.length === 0 ? ( - 706 |
- Plugin: vite:react-babel - File: D:/Cursor项目/GSM3/client/src/pages/TerminalPage.tsx:703:6 - 719| <> - 720| {/* 终端头部 */} - 721|
- | ^ - 722|
- 723|
- at constructor (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:367:19) - at TypeScriptParserMixin.raise (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:6627:19) - at TypeScriptParserMixin.unexpected (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:6647:16) - at TypeScriptParserMixin.expect (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:6927:12) - at TypeScriptParserMixin.parseParenAndDistinguishExpression (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11660:14) - at TypeScriptParserMixin.parseExprAtom (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11326:23) - at TypeScriptParserMixin.parseExprAtom (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:4794:20) - at TypeScriptParserMixin.parseExprSubscripts (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11076:23) - at TypeScriptParserMixin.parseUpdate (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11061:21) - at TypeScriptParserMixin.parseMaybeUnary (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11041:23) - at TypeScriptParserMixin.parseMaybeUnary (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:9852:18) - at TypeScriptParserMixin.parseMaybeUnaryOrPrivate (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10894:61) - at TypeScriptParserMixin.parseExprOpBaseRightExpr (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10981:34) - at TypeScriptParserMixin.parseExprOpRightExpr (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10976:21) - at TypeScriptParserMixin.parseExprOp (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10942:27) - at TypeScriptParserMixin.parseExprOp (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:9389:18) - at TypeScriptParserMixin.parseExprOps (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10903:17) - at TypeScriptParserMixin.parseMaybeConditional (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10876:23) - at TypeScriptParserMixin.parseMaybeAssign (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10826:21) - at TypeScriptParserMixin.parseMaybeAssign (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:9801:20) - at TypeScriptParserMixin.parseExpressionBase (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10779:23) - at D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10775:39 - at TypeScriptParserMixin.allowInAnd (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:12427:12) - at TypeScriptParserMixin.parseExpression (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10775:17) - at TypeScriptParserMixin.jsxParseExpressionContainer (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:4662:31) - at TypeScriptParserMixin.jsxParseElementAt (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:4741:36) - at TypeScriptParserMixin.jsxParseElement (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:4779:17) - at TypeScriptParserMixin.parseExprAtom (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:4789:19) - at TypeScriptParserMixin.parseExprSubscripts (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11076:23) - at TypeScriptParserMixin.parseUpdate (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11061:21) - at TypeScriptParserMixin.parseMaybeUnary (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11041:23) - at TypeScriptParserMixin.parseMaybeUnary (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:9852:18) - at TypeScriptParserMixin.parseMaybeUnaryOrPrivate (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10894:61) - at TypeScriptParserMixin.parseExprOps (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10899:23) - at TypeScriptParserMixin.parseMaybeConditional (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10876:23) - at TypeScriptParserMixin.parseMaybeAssign (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10826:21) - at D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:9790:39 - at TypeScriptParserMixin.tryParse (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:6935:20) - at TypeScriptParserMixin.parseMaybeAssign (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:9790:18) - at D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10795:39 - at TypeScriptParserMixin.allowInAnd (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:12427:12) - at TypeScriptParserMixin.parseMaybeAssignAllowIn (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:10795:17) - at TypeScriptParserMixin.parseMaybeAssignAllowInOrVoidPattern (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:12494:17) - at TypeScriptParserMixin.parseParenAndDistinguishExpression (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11674:28) - at TypeScriptParserMixin.parseExprAtom (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11326:23) - at TypeScriptParserMixin.parseExprAtom (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:4794:20) - at TypeScriptParserMixin.parseExprSubscripts (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11076:23) - at TypeScriptParserMixin.parseUpdate (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11061:21) - at TypeScriptParserMixin.parseMaybeUnary (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:11041:23) - at TypeScriptParserMixin.parseMaybeUnary (D:\Cursor项目\GSM3\client\node_modules\@babel\parser\lib\index.js:9852:18) \ No newline at end of file