diff --git a/docs/superpowers/plans/2026-04-12-admin-oss-refactor.md b/docs/superpowers/plans/2026-04-12-admin-oss-refactor.md index bb87bac..4247bb6 100644 --- a/docs/superpowers/plans/2026-04-12-admin-oss-refactor.md +++ b/docs/superpowers/plans/2026-04-12-admin-oss-refactor.md @@ -64,6 +64,201 @@ cd front && npm run build Expected: both commands pass. +### Task 9: Replace Hand-Styled Admin Search And Filter Inputs + +**Files:** +- Modify: `front/src/admin/users-list.tsx` +- Modify: `front/src/admin/files-list.tsx` +- Modify: `front/src/admin/shares.tsx` +- Modify: `front/src/admin/fileblobs.tsx` +- Modify: `front/src/admin/tasks.tsx` +- Modify: `front/src/admin/audits.tsx` + +- [ ] **Step 1: Replace search and filter inputs with the shared admin input** + +Target: + +```text +- 用户策略: 搜索框 +- 文件治理: 文件名 / 所属用户 +- 分享治理: 用户 / 文件名 / Token +- 对象治理: 用户 / 策略 ID / 对象键 +- 任务监控: 多个筛选输入 +- 审计日志: actor / action / targetType / targetId +``` + +- [ ] **Step 2: Verify Batch 9** + +Run: + +```bash +cd front && npm run lint +cd front && npm run build +``` + +Expected: both commands pass. + +### Task 8: Replace Hand-Styled Admin Text And Number Inputs + +**Files:** +- Add: `front/src/components/admin/AdminInput.tsx` +- Modify: `front/src/admin/users-list.tsx` + +> Current execution scope for this batch is limited to `users-list.tsx`. + +- [x] **Step 1: Add reusable admin input primitives** + +Expected: + +```text +- expose shared admin text/number input styling +- support form registration and controlled usage +- keep current focus, disabled, and validation affordances +``` + +- [x] **Step 2: Replace editable inputs in configuration-heavy pages** + +Target: + +```text +- 用户策略: 存储配额 / 最大上传限制 / 手动密码 +``` + +- [x] **Step 3: Verify Batch 8** + +Run: + +```bash +cd front && npm run lint +cd front && npm run build +``` + +Expected: both commands pass. + +### Task 6: Replace Hand-Rolled Admin Select Controls + +**Files:** +- Modify: `front/package.json` +- Modify: `front/package-lock.json` +- Add: `front/src/components/admin/AdminSelect.tsx` +- Modify: `front/src/admin/shares.tsx` +- Modify: `front/src/admin/fileblobs.tsx` +- Modify: `front/src/admin/tasks.tsx` +- Modify: `front/src/admin/storage-policies-list.tsx` + +- [x] **Step 1: Add a reusable admin select wrapper based on Radix Select** + +Expected: + +```text +- install an OSS select dependency +- expose a shared admin-styled select wrapper +- support placeholder, controlled value, and option groups used by admin pages +``` + +- [x] **Step 2: Replace filter selects in governance pages** + +Target: + +```text +- 分享治理: 密码保护 / 过期状态 +- 对象治理: 实体类型 +- 任务监控: 每页条数 +``` + +- [x] **Step 3: Replace modal selects in storage policies** + +Target: + +```text +- 新建 / 编辑策略: 驱动协议 +- 发起迁移: 目标策略选择 +``` + +- [x] **Step 4: Verify Batch 6** + +Run: + +```bash +cd front && npm run lint +cd front && npm run build +``` + +Expected: both commands pass. + +### Task 7: Replace The Remaining Admin Form Select + +**Files:** +- Modify: `front/src/admin/users-list.tsx` + +- [x] **Step 1: Replace the user role field with the shared admin select** + +Target: + +```text +- 用户策略: role 字段 +- 保持 react-hook-form 校验和提交逻辑不变 +``` + +- [x] **Step 2: Verify Batch 7** + +Run: + +```bash +cd front && npm run lint +cd front && npm run build +``` + +Expected: both commands pass. + +### Task 5: Replace Hand-Rolled Admin Modal Shells + +**Files:** +- Modify: `front/package.json` +- Modify: `front/package-lock.json` +- Add: `front/src/components/admin/AdminDialog.tsx` +- Modify: `front/src/admin/storage-policies-list.tsx` +- Modify: `front/src/admin/users-list.tsx` + +- [x] **Step 1: Add a reusable admin dialog wrapper based on Radix Dialog** + +Expected: + +```text +- install an OSS dialog dependency +- expose a shared admin-styled modal/sheet wrapper +- support centered modal and right-side panel layouts +``` + +- [x] **Step 2: Replace storage policy form and migration overlays** + +Target: + +```text +- 新建 / 编辑策略弹层 +- 发起迁移弹层 +``` + +- [x] **Step 3: Replace the user strategy editor shell** + +Target: + +```text +- 用对话框 / 侧栏承载用户策略编辑 +- 保留当前表单与快捷操作逻辑 +``` + +- [x] **Step 4: Verify Batch 5** + +Run: + +```bash +cd front && npm run lint +cd front && npm run build +``` + +Expected: both commands pass. + ### Task 2: Replace Admin List Tables With `@tanstack/react-table` **Files:** @@ -139,3 +334,58 @@ cd front && npm run build ``` Expected: both commands pass. + +### Task 4: Replace Native Admin Alerts And Confirms + +**Files:** +- Modify: `front/package.json` +- Modify: `front/package-lock.json` +- Add: `front/src/components/admin/AdminAlertDialog.tsx` +- Modify: `front/src/admin/settings.tsx` +- Modify: `front/src/admin/shares.tsx` +- Modify: `front/src/admin/files-list.tsx` +- Modify: `front/src/admin/filesystem.tsx` +- Modify: `front/src/admin/fileblobs.tsx` +- Modify: `front/src/admin/audits.tsx` + +- [x] **Step 1: Add a reusable admin alert dialog wrapper** + +Expected: + +```text +- install an OSS dialog dependency +- expose a shared admin-styled alert dialog wrapper +- keep current Tailwind visual system intact +``` + +- [x] **Step 2: Replace destructive confirm actions with in-app dialogs** + +Target: + +```text +- 系统设置: 轮换邀请码 +- 分享治理: 删除分享 +- 文件治理: 彻底删除 +``` + +- [x] **Step 3: Replace remaining admin alert feedback with page-level notices** + +Target: + +```text +- 文件系统: 复制失败 +- 对象实体: 复制失败 +- 审计日志: 复制失败 +- 分享治理: 复制成功 / 失败 +``` + +- [x] **Step 4: Verify Batch 4** + +Run: + +```bash +cd front && npm run lint +cd front && npm run build +``` + +Expected: both commands pass. diff --git a/front/package-lock.json b/front/package-lock.json index 1cb9466..962180d 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.0", "dependencies": { "@google/genai": "^1.29.0", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-table": "^8.21.3", "@vitejs/plugin-react": "^5.0.4", @@ -713,6 +716,44 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@google/genai": { "version": "1.49.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.49.0.tgz", @@ -845,6 +886,590 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1678,6 +2303,18 @@ "node": ">= 14" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -2003,6 +2640,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dotenv": { "version": "17.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", @@ -2463,6 +3106,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3382,6 +4034,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", @@ -3433,6 +4132,28 @@ "url": "https://opencollective.com/express" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -3845,6 +4566,49 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/front/package.json b/front/package.json index 7d8c6a9..16adc1b 100644 --- a/front/package.json +++ b/front/package.json @@ -12,6 +12,9 @@ }, "dependencies": { "@google/genai": "^1.29.0", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-table": "^8.21.3", "@vitejs/plugin-react": "^5.0.4", diff --git a/front/src/admin/audits.tsx b/front/src/admin/audits.tsx index ac7deaf..901b598 100644 --- a/front/src/admin/audits.tsx +++ b/front/src/admin/audits.tsx @@ -99,6 +99,7 @@ export default function AdminAuditsPage() { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(''); + const [notice, setNotice] = useState(''); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [page, setPage] = useState<{ items: AdminAuditLog[]; @@ -134,8 +135,11 @@ export default function AdminAuditsPage() { async function copyText(value: string) { try { await navigator.clipboard.writeText(value); + setNotice('审计详情已复制'); + setError(''); } catch { - window.alert('复制失败,请手动复制。'); + setError('复制失败,请手动复制。'); + setNotice(''); } } @@ -179,6 +183,12 @@ export default function AdminAuditsPage() { + {notice ? ( +
+ {notice} +
+ ) : null} +
{ event.preventDefault(); diff --git a/front/src/admin/fileblobs.tsx b/front/src/admin/fileblobs.tsx index 277b076..c5a268c 100644 --- a/front/src/admin/fileblobs.tsx +++ b/front/src/admin/fileblobs.tsx @@ -1,6 +1,7 @@ import { useEffect, useState, type ReactNode } from 'react'; import { AlertTriangle, CheckCircle2, Copy, FileBox, RefreshCw, Search, ShieldAlert, XCircle } from 'lucide-react'; import { motion } from 'motion/react'; +import { AdminSelect } from '@/src/components/admin/AdminSelect'; import { cn } from '@/src/lib/utils'; import { formatBytes, formatDateTime } from '@/src/lib/format'; import { @@ -114,6 +115,7 @@ export default function AdminFileBlobs() { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(''); + const [notice, setNotice] = useState(''); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [page, setPage] = useState<{ items: AdminFileBlobResponse[]; @@ -166,8 +168,11 @@ export default function AdminFileBlobs() { try { await navigator.clipboard.writeText(value); + setNotice('对象键已复制'); + setError(''); } catch { - window.alert('复制失败,请手动复制。'); + setError('复制失败,请手动复制。'); + setNotice(''); } } @@ -214,6 +219,12 @@ export default function AdminFileBlobs() { + {notice ? ( +
+ {notice} +
+ ) : null} + {metricCard({ @@ -285,7 +296,7 @@ export default function AdminFileBlobs() { /> diff --git a/front/src/admin/files-list.tsx b/front/src/admin/files-list.tsx index 474fc45..46326bf 100644 --- a/front/src/admin/files-list.tsx +++ b/front/src/admin/files-list.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { RefreshCw, Search, Trash2, Folder, FileText, ChevronRight } from 'lucide-react'; import { motion } from 'motion/react'; import { cn } from '@/src/lib/utils'; +import { AdminAlertDialog } from '@/src/components/admin/AdminAlertDialog'; import { deleteAdminFile, listAdminFiles, type AdminFile } from '@/src/lib/admin'; import { formatBytes, formatDateTime } from '@/src/lib/format'; @@ -26,6 +27,7 @@ export default function AdminFilesList() { const [query, setQuery] = useState(''); const [ownerQuery, setOwnerQuery] = useState(''); const [files, setFiles] = useState([]); + const [pendingDeleteFile, setPendingDeleteFile] = useState(null); async function loadFiles(nextQuery = query, nextOwnerQuery = ownerQuery) { setError(''); @@ -43,6 +45,21 @@ export default function AdminFilesList() { void loadFiles(); }, []); + async function handleConfirmDeleteFile() { + if (!pendingDeleteFile) { + return; + } + + const target = pendingDeleteFile; + setPendingDeleteFile(null); + try { + await deleteAdminFile(target.id); + await loadFiles(); + } catch (err) { + setError(err instanceof Error ? err.message : '彻底删除文件失败'); + } + } + return ( - - - - - ) : null} - - - {migratingPolicy ? ( -
- { + if (!nextOpen) { + setShowForm(false); + setEditingPolicy(null); + } + }} + footer={ + <> + + + + } + > +
+
+ + setForm((current) => ({ ...current, name: event.target.value }))} + /> +
+
+ + setForm((current) => ({ ...current, type: event.target.value as StoragePolicyUpsertPayload['type'] }))} + > + + + +
+
+ + setForm((current) => ({ ...current, endpoint: event.target.value }))} + /> +
+
+ + setForm((current) => ({ ...current, bucketName: event.target.value }))} + /> +
+
+ + + setForm((current) => ({ + ...current, + maxSizeBytes: Number(event.target.value), + capabilities: { ...current.capabilities, maxObjectSize: Number(event.target.value) }, + })) + } + /> +
+
-
-
-
源策略
-
{migratingPolicy.name}
-
PID::{migratingPolicy.id}
-
-
-
-
- - -
-
- - setMigrationTargetPolicyId(event.target.value)} - placeholder="例如 12" - className="w-full rounded-lg glass-panel bg-white/10 p-4 outline-none border-white/10 focus:border-blue-500/50 transition-all font-bold text-sm" - /> -
-
-

- 如果目标策略不在下拉框里,可以直接输入它的策略 ID。当前页面只负责创建迁移任务,不负责迁移进度展示。 -

-
+
+ {( + [ + ['privateBucket', '私有桶'], + ['enabled', '启用'], + ['capabilities.directUpload', '直传'], + ['capabilities.multipartUpload', '分片上传'], + ['capabilities.signedDownloadUrl', '签名下载'], + ['capabilities.serverProxyDownload', '代理下载'], + ['capabilities.requiresCors', '需要 CORS'], + ['capabilities.supportsInternalEndpoint', '内网端点'], + ] as const + ).map(([key, label]) => { + const checked = + key === 'privateBucket' + ? form.privateBucket + : key === 'enabled' + ? form.enabled + : form.capabilities[key.replace('capabilities.', '') as keyof StoragePolicyCapabilities]; + const checkedBoolean = Boolean(checked); + return ( + + ); + })} +
+ -
- + + + } + > + {migratingPolicy ? ( +
+
+
源策略
+
{migratingPolicy.name}
+
PID::{migratingPolicy.id}
+
+
+
+
+ + setMigrationTargetPolicyId(event.target.value)} > - 取消 - - + + {policies + .filter((item) => item.id !== migratingPolicy.id) + .map((policy) => ( + + ))} +
- +
+ + setMigrationTargetPolicyId(event.target.value)} + placeholder="例如 12" + /> +
+
+

+ 如果目标策略不在下拉框里,可以直接输入它的策略 ID。当前页面只负责创建迁移任务,不负责迁移进度展示。 +

) : null} - + ); } diff --git a/front/src/admin/tasks.tsx b/front/src/admin/tasks.tsx index ecb37d2..b7d04c4 100644 --- a/front/src/admin/tasks.tsx +++ b/front/src/admin/tasks.tsx @@ -12,6 +12,7 @@ import { User, } from 'lucide-react'; import { motion } from 'motion/react'; +import { AdminSelect } from '@/src/components/admin/AdminSelect'; import { cn } from '@/src/lib/utils'; import { formatDateTime } from '@/src/lib/format'; import { getAdminTask, getAdminTasks, type AdminTask, type AdminTaskQuery } from '@/src/lib/admin-tasks'; @@ -345,21 +346,21 @@ export default function AdminTasks() {
- ) : null} + + + {watchedRole} +
- {editingUser ? ( -
-
-
-
-
当前账号
-
{editingUser.email}
-
- - {editingUser.banned ? '已禁用' : '正常'} - -
-
- {formatBytes(editingUser.usedStorageBytes)} / {formatBytes(editingUser.storageQuotaBytes)} -
-
-
-
-
- -
-
-
-
基础配置
-
修改角色、存储配额和最大上传限制后,点击保存即可生效。
-
- - - {watchedRole} - -
- - - -
- - -
- - -
+ + + + )} + /> + {errors.role ? ( +

{errors.role.message}

+ ) : null} + -
-
-
-
手动设置密码
-
- 这里是人工指定一个新密码,会直接覆盖当前密码。它和“生成临时密码”是两条不同的管理路径。 -
-
- -
- - -

- 适合人工恢复账号、统一初始化密码或和用户同步已知密码。若要发放一次性密码,请继续使用表格里的“生成临时密码”。 +

+
+ ) : null} + + +
-
-
账号状态
-
- 可直接切换禁用 / 恢复,不影响上面的基础配置或手动改密表单。 -
- + +
+ +
+
+
+
手动设置密码
+
+ 这里是人工指定一个新密码,会直接覆盖当前密码。它和“生成临时密码”是两条不同的管理路径。
- ) : ( -
-
编辑面板为空
-

- 点击左侧任意用户行的“编辑”按钮,右侧会自动展开该用户的角色、配额和密码管理表单。 + +

+
- )} - - - )} -
+ ) : null} + + +

+ 适合人工恢复账号、统一初始化密码或和用户同步已知密码。若要发放一次性密码,请继续使用表格里的“生成临时密码”。 +

+
+ +
+
账号状态
+
+ 可直接切换禁用 / 恢复,不影响上面的基础配置或手动改密表单。 +
+ +
+
+ ) : null} + ); } diff --git a/front/src/components/admin/AdminAlertDialog.tsx b/front/src/components/admin/AdminAlertDialog.tsx new file mode 100644 index 0000000..0a1489c --- /dev/null +++ b/front/src/components/admin/AdminAlertDialog.tsx @@ -0,0 +1,93 @@ +import * as AlertDialog from '@radix-ui/react-alert-dialog'; +import { AlertTriangle } from 'lucide-react'; +import { cn } from '@/src/lib/utils'; + +type AdminAlertDialogProps = { + open: boolean; + title: string; + description: string; + confirmLabel?: string; + cancelLabel?: string; + confirmTone?: 'danger' | 'warning'; + busy?: boolean; + onConfirm: () => void | Promise; + onCancel: () => void; +}; + +export function AdminAlertDialog({ + open, + title, + description, + confirmLabel = '确认', + cancelLabel = '取消', + confirmTone = 'danger', + busy = false, + onConfirm, + onCancel, +}: AdminAlertDialogProps) { + const toneClasses = + confirmTone === 'warning' + ? 'border-amber-500/20 bg-amber-500/10 text-amber-400' + : 'border-red-500/20 bg-red-500/10 text-red-400'; + + return ( + { + if (!nextOpen && !busy) { + onCancel(); + } + }} + > + + + { + if (busy) { + event.preventDefault(); + } + }} + className="fixed inset-0 z-[101] flex items-center justify-center px-4 py-6" + > +
+
+ +
+ + {title} + + {description} + + +
+ + + + + + +
+
+
+
+
+ ); +} diff --git a/front/src/components/admin/AdminDialog.tsx b/front/src/components/admin/AdminDialog.tsx new file mode 100644 index 0000000..5b5bacd --- /dev/null +++ b/front/src/components/admin/AdminDialog.tsx @@ -0,0 +1,187 @@ +import * as Dialog from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { cn } from '@/src/lib/utils'; + +export const AdminDialogRoot = Dialog.Root; +export const AdminDialogTrigger = Dialog.Trigger; +export const AdminDialogPortal = Dialog.Portal; +export const AdminDialogOverlay = Dialog.Overlay; +export const AdminDialogContent = Dialog.Content; +export const AdminDialogTitle = Dialog.Title; +export const AdminDialogDescription = Dialog.Description; +export const AdminDialogClose = Dialog.Close; + +type AdminDialogLayout = 'center' | 'side-panel'; +type AdminDialogAccent = 'default' | 'warning' | 'danger' | 'success'; + +type AdminDialogProps = { + open: boolean; + title: ReactNode; + description?: ReactNode; + icon?: ReactNode; + layout?: AdminDialogLayout; + mode?: AdminDialogLayout; + size?: string; + accent?: AdminDialogAccent; + dismissible?: boolean; + showCloseButton?: boolean; + footer?: ReactNode; + children: ReactNode; + onOpenChange: (open: boolean) => void; + className?: string; + overlayClassName?: string; + panelClassName?: string; + headerClassName?: string; + bodyClassName?: string; + footerClassName?: string; + titleClassName?: string; + descriptionClassName?: string; + closeLabel?: string; +}; + +const accentClasses: Record = { + default: 'border-blue-500/20 bg-blue-500/10 text-blue-400', + warning: 'border-amber-500/20 bg-amber-500/10 text-amber-400', + danger: 'border-red-500/20 bg-red-500/10 text-red-400', + success: 'border-green-500/20 bg-green-500/10 text-green-400', +}; + +export function AdminDialog({ + open, + title, + description, + icon, + layout, + mode, + size, + accent = 'default', + dismissible = true, + showCloseButton = true, + footer, + children, + onOpenChange, + className, + overlayClassName, + panelClassName, + headerClassName, + bodyClassName, + footerClassName, + titleClassName, + descriptionClassName, + closeLabel = '关闭对话框', +}: AdminDialogProps) { + const resolvedLayout = layout ?? mode ?? (size === 'side-panel' ? 'side-panel' : 'center'); + const isSidePanel = resolvedLayout === 'side-panel'; + const sizeClasses = + size === 'sm' + ? 'max-w-[min(96vw,28rem)]' + : size === 'md' + ? 'max-w-[min(96vw,36rem)]' + : size === 'xl' + ? 'max-w-[min(96vw,56rem)]' + : size === '2xl' + ? 'max-w-[min(96vw,72rem)]' + : size === 'full' + ? 'max-w-[calc(100vw-2rem)]' + : 'max-w-[min(96vw,48rem)]'; + const shellClasses = cn( + 'glass-panel-no-hover relative flex min-h-0 w-full flex-col overflow-hidden text-gray-900 dark:text-gray-100', + isSidePanel + ? 'h-[100dvh] max-w-[min(100vw,40rem)] rounded-none border-l border-white/10 shadow-[0_30px_120px_rgba(0,0,0,0.55)]' + : cn('max-h-[min(calc(100dvh-3rem),48rem)] rounded-3xl border border-white/10 shadow-[0_30px_120px_rgba(0,0,0,0.55)]', sizeClasses), + panelClassName, + ); + + return ( + + + + { + if (!dismissible) { + event.preventDefault(); + } + }} + onPointerDownOutside={(event) => { + if (!dismissible) { + event.preventDefault(); + } + }} + onInteractOutside={(event) => { + if (!dismissible) { + event.preventDefault(); + } + }} + > +
+
+
+
+ {icon ? ( +
+ {icon} +
+ ) : null} +
+ + {title} + + {description ? ( + + {description} + + ) : null} +
+
+
+ + {showCloseButton ? ( + + + + ) : null} +
+ +
{children}
+ + {footer ? ( +
{footer}
+ ) : null} +
+
+
+
+ ); +} diff --git a/front/src/components/admin/AdminInput.tsx b/front/src/components/admin/AdminInput.tsx new file mode 100644 index 0000000..1863c29 --- /dev/null +++ b/front/src/components/admin/AdminInput.tsx @@ -0,0 +1,21 @@ +import { forwardRef, type InputHTMLAttributes } from 'react'; +import { cn } from '@/src/lib/utils'; + +export type AdminInputProps = InputHTMLAttributes; + +export const AdminInput = forwardRef(function AdminInput( + { className, type = 'text', ...props }, + ref, +) { + return ( + + ); +}); diff --git a/front/src/components/admin/AdminSelect.tsx b/front/src/components/admin/AdminSelect.tsx new file mode 100644 index 0000000..f1822ad --- /dev/null +++ b/front/src/components/admin/AdminSelect.tsx @@ -0,0 +1,155 @@ +import * as Select from '@radix-ui/react-select'; +import { Check, ChevronDown, ChevronUp } from 'lucide-react'; +import { Children, isValidElement, type ReactNode } from 'react'; +import { cn } from '@/src/lib/utils'; + +type AdminSelectChangeEvent = { + target: { + value: string; + }; +}; + +type ParsedOption = { + value: string; + label: string; + disabled: boolean; +}; + +type WidthPreset = 'field' | 'filter' | 'compact' | 'fit'; + +type AdminSelectProps = { + value: string | number; + onChange: (event: AdminSelectChangeEvent) => void; + children: ReactNode; + className?: string; + disabled?: boolean; + width?: WidthPreset; +}; + +const EMPTY_VALUE_SENTINEL = '__ADMIN_SELECT_EMPTY__'; + +function stringifyNode(node: ReactNode): string { + if (typeof node === 'string' || typeof node === 'number') { + return String(node); + } + + if (Array.isArray(node)) { + return node.map((item) => stringifyNode(item)).join('').trim(); + } + + if (isValidElement<{ children?: ReactNode }>(node)) { + return stringifyNode(node.props.children); + } + + return ''; +} + +function parseOptions(children: ReactNode): { options: ParsedOption[]; hasEmptyOption: boolean } { + const options: ParsedOption[] = []; + let hasEmptyOption = false; + + Children.forEach(children, (child) => { + if (!isValidElement<{ value?: string | number; disabled?: boolean; children?: ReactNode }>(child)) { + return; + } + + const rawValue = child.props.value; + const value = rawValue == null ? '' : String(rawValue); + if (value === '') { + hasEmptyOption = true; + } + + options.push({ + value, + label: stringifyNode(child.props.children), + disabled: Boolean(child.props.disabled), + }); + }); + + return { options, hasEmptyOption }; +} + +export function AdminSelect({ + value, + onChange, + children, + className, + disabled = false, + width = 'field', +}: AdminSelectProps) { + const { options, hasEmptyOption } = parseOptions(children); + const normalizedValue = String(value ?? ''); + const resolvedValue = normalizedValue === '' && hasEmptyOption ? EMPTY_VALUE_SENTINEL : normalizedValue; + const selectedOption = options.find((option) => option.value === normalizedValue); + const placeholder = options.find((option) => option.value === '')?.label ?? '请选择'; + + return ( + { + const resolvedNextValue = nextValue === EMPTY_VALUE_SENTINEL ? '' : nextValue; + onChange({ + target: { + value: resolvedNextValue, + }, + }); + }} + > + + + {selectedOption?.label} + + + + + + + + + + + + + {options.map((option) => { + const optionValue = option.value === '' ? EMPTY_VALUE_SENTINEL : option.value; + + return ( + + {option.label} + + + + + ); + })} + + + + + + + + ); +} diff --git a/memory.md b/memory.md index 31b2fec..2950892 100644 --- a/memory.md +++ b/memory.md @@ -6,13 +6,16 @@ - 项目主线已经从旧教务模块切换为“网盘 + 快传 + 管理台”结构 - 快传模块已整合进主站,支持取件码、分享链接、P2P 传输、部分文件接收、ZIP 下载、存入网盘 - 网盘已支持上传、下载、重命名、删除、移动、复制、公开分享、接收快传后存入 + - 2026-04-12 已将管理台治理页里的原生 `` 已切到共享 `AdminSelect`: +- 新建 / 编辑策略里的驱动协议选择 +- 发起迁移里的目标策略选择 +- 由于当前仓库里还没有现成的 `AdminSelect` 文件,我补了一个本地共享包装组件到 `front/src/components/admin/AdminSelect.tsx`,用原生 select 保持行为不变。 +- 其他页面、包依赖和后端调用均未改动。 +- OSS 替换执行计划 `docs/superpowers/plans/2026-04-12-admin-oss-refactor.md` 已继续打钩更新,Task 6 的 storage-policy 子步骤已完成。 +- 本批前端验证通过: +- `cd front && npm run lint` +- `cd front && npm run build` + +## 2026-04-12 Frontend OSS Refactor Batch 40 + +- 前端 OSS 替换计划的 Batch 6 已完成,管理台当前目标范围内的手写单选下拉已切到 Radix Select。 +- `front/package.json` / `front/package-lock.json` 已新增 `@radix-ui/react-select` 依赖。 +- 新增 `front/src/components/admin/AdminSelect.tsx`,当前是一个基于 Radix Select 的共享单选包装组件: +- 兼容当前页面已有的 `