diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md index aed1716..7233ec3 100644 --- a/.claude/skills/commit/SKILL.md +++ b/.claude/skills/commit/SKILL.md @@ -21,9 +21,10 @@ user-invocable: true 4. 检查当前 staged 变更。 5. 如果没有 staged 变更,先将当前工作区改动加入暂存区。 6. 如果 staged 或当前工作区改动中包含 `package.json` 或 `package-lock.json`,需要检查变更内容: - - 如果 `package-lock.json` 有修改 **或者** `package.json` 的 `dependencies` 或 `devDependencies` 部分有修改,必须先执行 `/linux-native-binding-repair`,确认不存在 Linux native binding、optionalDependencies 或 lockfile 跨平台漂移问题。 - - 如果只有 `package.json` 的其他部分(如 scripts、workspaces 等)修改,且 `package-lock.json` 没有修改,则不需要执行 `/linux-native-binding-repair`。 + - 如果 `package-lock.json` 有修改 **或者** `package.json` 的 `dependencies` 或 `devDependencies` 部分有修改,必须先执行 `/install-native-binding`。 + - 如果只有 `package.json` 的其他部分(如 scripts、workspaces 等)修改,且 `package-lock.json` 没有修改,则不需要执行 `/install-native-binding`。 7. 基于暂存区差异生成 commit message,格式必须是 `(): `。 + - 如果 staged 文件数量 **> 8**,在 subject 行之后追加 commit message body,列出各核心改动摘要(每行一条,格式 `- `)。 8. 如果用户传入参数,将其作为 subject,仍需自动补全 type 和 scope。 9. 执行 git 提交。 10. 返回以下结果: diff --git a/.claude/skills/install-native-binding/SKILL.md b/.claude/skills/install-native-binding/SKILL.md new file mode 100644 index 0000000..1537732 --- /dev/null +++ b/.claude/skills/install-native-binding/SKILL.md @@ -0,0 +1,72 @@ +--- +name: install-native-binding +description: '安装 npm 依赖并确保 Linux native binding 正确生成。在非 Linux 系统上通过 docker/podman 以 linux/amd64 容器执行安装,保证 lockfile 中包含跨平台 native binding。Use when installing dependencies, fixing missing native bindings, or syncing package-lock.json for Linux environments.' +user-invocable: true +--- + +# Install Native Binding Skill + +## 适用场景 + +- 安装/更新 npm 依赖后需要确保 Linux native binding 正确写入 lockfile +- 在 macOS / Windows 开发机上需要为 Linux 生产环境生成正确的 lockfile +- CI 报告缺少 native binding 或 optionalDependencies 平台条目 + +## 执行步骤 + +### 1. 检测当前操作系统 + +运行 `uname -s` 获取系统类型。 + +- 若输出为 `Linux`,进入**步骤 2(Linux 路径)**。 +- 否则(macOS、Windows 等),进入**步骤 3(非 Linux 路径)**。 + +### 2. Linux 路径 + +直接在当前环境运行: + +```bash +npm i --include-workspace-root --workspaces +``` + +完成后跳至**步骤 5**。 + +### 3. 非 Linux 路径 —— 检测容器运行时 + +依次检查以下命令是否可用: + +```bash +command -v docker +command -v podman +``` + +- 若两者均不可用,**停止执行**,告知用户需要安装 Docker 或 Podman 后重试。 +- 优先使用 `docker`;若 `docker` 不可用则使用 `podman`(以下统称 ``)。 + +### 4. 非 Linux 路径 —— 读取 Node 版本并在容器中安装 + +#### 4a. 读取 Node 版本 + +读取仓库根目录的 `.nvmrc` 文件,获取版本号(去掉前缀 `v`),作为镜像 tag。 + +例如 `.nvmrc` 内容为 `v24.12.0`,则镜像为 `node:24.12.0`。 + +#### 4b. 在容器中执行安装 + +```bash + run --rm --platform linux/amd64 \ + -v "$PWD":/src \ + --tmpfs /src/node_modules:exec \ + node: \ + bash -lc 'cd /src && npm i --include-workspace-root --workspaces' +``` + +- 挂载当前工作目录为 `/src`(读写,不加 `:ro`),使安装结果(`node_modules`、`package-lock.json`)直接写回宿主机。 +- `--tmpfs /src/node_modules:exec` 将容器内的根 `node_modules` 挂载为内存文件系统,避免宿主机的 `node_modules` 被覆盖,同时加速安装;`:exec` 允许 postinstall 脚本执行二进制文件(Docker tmpfs 默认带 `noexec`)。 +- `--platform linux/amd64` 确保 native binding 以 Linux x64 平台编译/下载。 + +## 约束 + +- 容器挂载目录为当前工作区根目录(即含 `package.json` 和 `package-lock.json` 的目录) +- 不要在容器内部 `cp` 文件;直接挂载读写以避免权限问题 +- Node 镜像版本必须严格来自 `.nvmrc`,不得硬编码或猜测 diff --git a/.claude/skills/linux-native-binding-repair/SKILL.md b/.claude/skills/linux-native-binding-repair/SKILL.md deleted file mode 100644 index 685ba3f..0000000 --- a/.claude/skills/linux-native-binding-repair/SKILL.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: linux-native-binding-repair -description: '排查并修复 Linux 环境下缺失 native binding、optionalDependencies 或 package-lock 跨平台漂移问题。Use when CI fails on Ubuntu/Linux, act fails on Linux containers, or errors mention missing native bindings.' -argument-hint: '可选填写失败包名、平台、错误信息、失败步骤或 CI 上下文' -user-invocable: true ---- - -# Linux Native Binding Repair - -## 适用场景 - -- GitHub Actions 在 Ubuntu 上失败,但本地 macOS 正常 -- `act` 失败,但本地 `npm run lint`、`npm run test` 或 `npm run build -- --force` 正常 -- 错误信息包含 `Cannot find native binding` -- 错误信息包含 `npm has a bug related to optional dependencies` -- 缺少 `@oxlint/binding-linux-x64-gnu`、`@oxfmt/binding-linux-x64-gnu`、`@rolldown/binding-linux-x64-gnu` 或同类平台包 -- `package-lock.json` 在 macOS 上生成,Linux CI 无法加载原生二进制 - -## 执行步骤 - -1. 先确认失败的包、运行环境和失败步骤。 -2. 记录缺失模块名,以及失败发生在 `npm ci`、`lint`、`test` 还是 `build`。 -3. 区分 `linux/amd64` 与 `linux/arm64`。 -4. 确认 CI 实际平台: - - GitHub Actions 的 `ubuntu-latest` 默认通常是 `linux/amd64` - - Apple Silicon 上的 `act` 可能默认是 `linux/arm64`,除非显式使用 `--container-architecture linux/amd64` -5. 检查根 `package-lock.json`,不要只检查子包 lockfile。 -6. 在根 lockfile 中搜索缺失 binding,并确认 `packages` 区域存在真实节点,例如 `node_modules/@scope/binding-linux-x64-gnu`,而不只是 `optionalDependencies` 引用。 -7. 运行前先检查本机是否安装 `docker` 与 `act`: - - `command -v docker` - - `command -v act` -8. 如果缺少 Docker,明确说明无法做容器级 Linux 复现,应优先安装 Docker;缺少 `act` 时,可退化为 Docker 验证。 -9. 优先使用与 CI 架构一致的 Docker 环境复现。 -10. 若要与 GitHub Actions x64 Linux 对齐,优先执行: - -```bash -docker run --rm --platform linux/amd64 -v "$PWD":/src:ro node:24.12.0-bookworm bash -lc 'cp -a /src /work && cd /work && npm ci --workspaces --include-workspace-root && npm run lint && npm run test' -``` - -11. 如果 binding 缺失,继续检查包元数据: - - 查看失败包在 `package-lock.json` 中的 `optionalDependencies` - - 从 npm registry 查询缺失 binding 包元数据: - -```bash -npm view @scope/binding-linux-x64-gnu@ dist.tarball dist.integrity cpu os engines --json -``` - -12. 修复根 `package-lock.json`: - - 优先在目标平台上重新生成 lockfile - - 如果 npm 仍未生成所需 `packages` 节点,可手动补齐缺失 Linux binding 节点,至少包含:`version`、`resolved`、`integrity`、`cpu`、`os`、`engines`、`optional: true` - - 如果父包是开发依赖,补 `dev: true` -13. 在目标平台重新验证: - - 重新执行 `npm ci` - - 重新执行此前失败的精确命令 - - 必要时单独验证对应 `act` job -14. 区分真实依赖问题与 `act` 兼容性限制: - - 若 `act` 报 `runs.using key ... got node24`,这是 `act` 的运行时兼容性限制,不是 binding 缺失 - -## 仓库特定说明 - -- 此仓库已经出现过 `oxfmt`、`oxlint`、`rolldown` 的 Linux binding 缺失问题 -- 修复应落在根 `package-lock.json` -- 主 CI 工作流是 `.github/workflows/push.yml` -- 相关复用工作流包括: - - `.github/workflows/monkey_push.yml` - - `.github/workflows/qinglong_push.yml` - -## 快速检查清单 - -- 已确认缺失的包名 -- 已确认 CI 架构 -- 已检查根 lockfile -- 已确认本机存在 `docker` -- 已确认本机是否存在 `act` -- 已用 Docker 在目标平台复现 -- 已从 npm 获取缺失 binding 的元数据 -- 已修复或重建根 lockfile -- 已在目标平台重新验证 `npm ci` -- 已区分 `act` 限制与真实依赖故障 - -## 常见需要检查的包 - -- `@oxlint/binding-linux-x64-gnu` -- `@oxfmt/binding-linux-x64-gnu` -- `@rolldown/binding-linux-x64-gnu` -- 以及根据 runner 架构变化的其他包,例如 `binding-linux-arm64-gnu`、`binding-linux-x64-musl` 等 diff --git a/.github/workflows/caddyfile_sdk.yml b/.github/workflows/caddyfile_sdk.yml index 29f25a2..03990fd 100644 --- a/.github/workflows/caddyfile_sdk.yml +++ b/.github/workflows/caddyfile_sdk.yml @@ -30,7 +30,7 @@ jobs: cache-dependency-path: package-lock.json - name: Install dependencies - run: npm ci -ws --include-workspace-root + run: npm ci --workspaces --include-workspace-root - name: Lint run: npm run lint diff --git a/.github/workflows/cloud_storage.yml b/.github/workflows/cloud_storage.yml new file mode 100644 index 0000000..20f9136 --- /dev/null +++ b/.github/workflows/cloud_storage.yml @@ -0,0 +1,48 @@ +name: Cloud Storage + +on: + push: + paths: + - apps/cf-worker/cloud-storage/** + - package.json + - package-lock.json + - tsconfig.json + - turbo.json + pull_request: + paths: + - apps/cf-worker/cloud-storage/** + - package.json + - package-lock.json + - tsconfig.json + - turbo.json + +jobs: + cloud-storage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.12.0 + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci --workspaces --include-workspace-root + + - name: Lint + run: npm run lint + env: + TURBO_FILTER: ./apps/cf-worker/cloud-storage + + - name: Test + run: npm run test + env: + TURBO_FILTER: ./apps/cf-worker/cloud-storage + + - name: Build + run: npm run build --ignore-scripts + env: + TURBO_FILTER: ./apps/cf-worker/cloud-storage diff --git a/.vscode/mcp.json b/.vscode/mcp.json index ebdacff..2281fd9 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,11 +1,25 @@ { - "servers": { - "git": { - "command": "bash", - "args": [ - "-c", - "(nvm -h || source $HOME/.nvm/nvm.sh) && (nvm use 20 || nvm install 20) && nvm exec 20 npx -y @cyanheads/git-mcp-server" - ] - } - } + "servers": { + "git": { + "command": "npx", + "args": [ + "-y", + "@cyanheads/git-mcp-server" + ] + }, + "chrome-devtools": { + "command": "npx", + "args": [ + "-y", + "chrome-devtools-mcp" + ] + }, + "shadcn": { + "command": "npx", + "args": [ + "shadcn@latest", + "mcp" + ] + } + } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6a1bf69..fdfe8b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,13 @@ { "cSpell.words": [ - "arylo" + "arylo", + "qinglong", + "syncpack" ], "files.associations": { "wrangler.json": "jsonc" - } + }, + "chat.tools.terminal.autoApprove": { + "command": true + } } diff --git a/CLAUDE.md b/CLAUDE.md index 92f2457..711c8e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,6 @@ ## Claude Skills - `/commit`:根据当前改动生成并执行规范化 Git 提交 -- `/linux-native-binding-repair`:排查并修复 Linux 环境下缺失的 native binding、optionalDependencies 和 package-lock 跨平台漂移问题 +- `/install-native-binding`:安装 npm 依赖并确保 Linux native binding 正确生成(非 Linux 系统自动通过 docker/podman 容器执行) 以上能力已迁移到 `.claude/skills/`,按 Claude skill 方式维护与发现。 diff --git a/apps/caddyfile-sdk/package.json b/apps/caddyfile-sdk/package.json index 477a938..fed1dff 100644 --- a/apps/caddyfile-sdk/package.json +++ b/apps/caddyfile-sdk/package.json @@ -46,9 +46,9 @@ "devDependencies": { "concurrently": "^8.2.2", "oxfmt": "^0.35.0", - "oxlint": "^1.24.0", - "rimraf": "^6.0.1", + "oxlint": "^1.58.0", + "rimraf": "^6.1.3", "typescript": "~5.9.3", - "vitest": "^4.1.0" + "vitest": "^4.1.4" } } diff --git a/apps/cf-worker/cloud-storage/.oxfmtrc.json b/apps/cf-worker/cloud-storage/.oxfmtrc.json new file mode 100644 index 0000000..44589c4 --- /dev/null +++ b/apps/cf-worker/cloud-storage/.oxfmtrc.json @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "semi": false, + "sortImports": { + "newlinesBetween": false + }, + "ignorePatterns": ["*.md", "dist/**", ".wrangler/**", "src/react-app/Components/**"] +} diff --git a/apps/cf-worker/cloud-storage/.oxlintrc.json b/apps/cf-worker/cloud-storage/.oxlintrc.json new file mode 100644 index 0000000..a096fc9 --- /dev/null +++ b/apps/cf-worker/cloud-storage/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../../node_modules/oxlint/configuration_schema.json", + "plugins": ["import"], + "env": { + "browser": true, + "es6": true + }, + "ignorePatterns": ["dist/**", "node_modules/**", "src/react-app/Components/**"], + "rules": {} +} diff --git a/apps/cf-worker/cloud-storage/.vscode/settings.json b/apps/cf-worker/cloud-storage/.vscode/settings.json new file mode 100644 index 0000000..9466963 --- /dev/null +++ b/apps/cf-worker/cloud-storage/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "wrangler.json": "jsonc" + } +} diff --git a/apps/cf-worker/cloud-storage/AGENTS.md b/apps/cf-worker/cloud-storage/AGENTS.md new file mode 100644 index 0000000..93e7d3d --- /dev/null +++ b/apps/cf-worker/cloud-storage/AGENTS.md @@ -0,0 +1,34 @@ +# Cloudflare Workers + +STOP. Your knowledge of Cloudflare Workers APIs and limits may be outdated. Always retrieve current documentation before any Workers, KV, R2, D1, Durable Objects, Queues, Vectorize, AI, or Agents SDK task. + +## Docs + +- https://developers.cloudflare.com/workers/ +- MCP: `https://docs.mcp.cloudflare.com/mcp` + +For all limits and quotas, retrieve from the product's `/platform/limits/` page. eg. `/workers/platform/limits` + +## Commands + +| Command | Purpose | +|---------|---------| +| `npx wrangler dev` | Local development | +| `npx wrangler deploy` | Deploy to Cloudflare | +| `npx wrangler types` | Generate TypeScript types | + +Run `wrangler types` after changing bindings in wrangler.jsonc. + +## Node.js Compatibility + +https://developers.cloudflare.com/workers/runtime-apis/nodejs/ + +## Errors + +- **Error 1102** (CPU/Memory exceeded): Retrieve limits from `/workers/platform/limits/` +- **All errors**: https://developers.cloudflare.com/workers/observability/errors/ + +## Product Docs + +Retrieve API references and limits from: +`/kv/` · `/r2/` · `/d1/` · `/durable-objects/` · `/queues/` · `/vectorize/` · `/workers-ai/` · `/agents/` diff --git a/apps/cf-worker/cloud-storage/CLAUDE.md b/apps/cf-worker/cloud-storage/CLAUDE.md new file mode 100644 index 0000000..6f4edda --- /dev/null +++ b/apps/cf-worker/cloud-storage/CLAUDE.md @@ -0,0 +1,14 @@ +# CLAUDE.md + +## 文档列表 + +- `目录结构`: ./docs/project_structure.md +- `项目逻辑`: ./docs/project_logic.md +- `项目路线图`: ./Roadmap.md +- `库 / 框架文档位置`: ./docs/libraries.md +- `名词对照表`: ./docs/glossary.md + +### 使用教程 + +- `提取页面`: ./docs/pickup_guide.md +- `管理页面`: ./docs/admin_guide.md diff --git a/apps/cf-worker/cloud-storage/README.md b/apps/cf-worker/cloud-storage/README.md new file mode 100644 index 0000000..33d06ac --- /dev/null +++ b/apps/cf-worker/cloud-storage/README.md @@ -0,0 +1,137 @@ +# React + Vite + Hono + Cloudflare Workers + +[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/vite-react-template) + +This template provides a minimal setup for building a React application with TypeScript and Vite, designed to run on Cloudflare Workers. It features hot module replacement, ESLint integration, and the flexibility of Workers deployments. + +![React + TypeScript + Vite + Cloudflare Workers](https://imagedelivery.net/wSMYJvS3Xw-n339CbDyDIA/fc7b4b62-442b-4769-641b-ad4422d74300/public) + + + +🚀 Supercharge your web development with this powerful stack: + +- [**React**](https://react.dev/) - A modern UI library for building interactive interfaces +- [**Vite**](https://vite.dev/) - Lightning-fast build tooling and development server +- [**Hono**](https://hono.dev/) - Ultralight, modern backend framework +- [**Cloudflare Workers**](https://developers.cloudflare.com/workers/) - Edge computing platform for global deployment + +### ✨ Key Features + +- 🔥 Hot Module Replacement (HMR) for rapid development +- 📦 TypeScript support out of the box +- 🛠️ ESLint configuration included +- ⚡ Zero-config deployment to Cloudflare's global network +- 🎯 API routes with Hono's elegant routing +- 🔄 Full-stack development setup +- 🔎 Built-in Observability to monitor your Worker + +Get started in minutes with local development or deploy directly via the Cloudflare dashboard. Perfect for building modern, performant web applications at the edge. + + + +## Getting Started + +To start a new project with this template, run: + +```bash +npm create cloudflare@latest -- --template=cloudflare/templates/vite-react-template +``` + +A live deployment of this template is available at: +[https://react-vite-template.templates.workers.dev](https://react-vite-template.templates.workers.dev) + +## Development + +Install dependencies: + +```bash +npm install +``` + +Start the development server with: + +```bash +npm run dev +``` + +Your application will be available at [http://localhost:5173](http://localhost:5173). + +## Production + +Build your project for production: + +```bash +npm run build +``` + +Preview your build locally: + +```bash +npm run preview +``` + +Deploy your project to Cloudflare Workers: + +```bash +npm run build && npm run deploy +``` + +Monitor your workers: + +```bash +npx wrangler tail +``` + +## R2 Bucket 绑定 + +此项目已配置 R2 bucket 绑定,名为 `STORAGE_BUCKET`。你可以在 Worker 代码中通过 `c.env.STORAGE_BUCKET` 访问 R2 bucket。 + +### 可用 API 端点 + +1. **列出文件** - `GET /api/files` + ```bash + curl http://localhost:8787/api/files + ``` +3. **获取文件** - `GET /api/files/:key` + ```bash + curl http://localhost:8787/api/files/your-file-key + ``` + +### 配置 R2 Bucket + +1. 在 Cloudflare Dashboard 中创建 R2 bucket: + - 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/) + - 导航到 R2 → Create bucket + - 输入 bucket 名称(例如:`cloud-storage-bucket`) + +2. 更新 `wrangler.json` 中的 bucket 名称(如果需要): + ```json + "r2_buckets": [ + { + "binding": "STORAGE_BUCKET", + "bucket_name": "your-bucket-name" + } + ] + ``` + +3. 部署 Worker: + ```bash + npm run build && npm run deploy + ``` + +### TypeScript 类型 + +运行以下命令生成类型定义: +```bash +npx wrangler types +``` + +这将在 `worker-configuration.d.ts` 中生成 `STORAGE_BUCKET: R2Bucket` 类型。 + +## Additional Resources + +- [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/) +- [Cloudflare R2 Documentation](https://developers.cloudflare.com/r2/) +- [Vite Documentation](https://vitejs.dev/guide/) +- [React Documentation](https://reactjs.org/) +- [Hono Documentation](https://hono.dev/) diff --git a/apps/cf-worker/cloud-storage/Roadmap.md b/apps/cf-worker/cloud-storage/Roadmap.md new file mode 100644 index 0000000..2c910d3 --- /dev/null +++ b/apps/cf-worker/cloud-storage/Roadmap.md @@ -0,0 +1,45 @@ +# Road Map + +## v1.1 + +### 管理端 + +- [ ] 文件短链管理: 创建、列表、删除、禁止、最近使用时间 +- [ ] 分享盘管理: 最近使用时间 +- [ ] 提取码管理: 最近使用时间、支持自定义提取码(至少8位混合字母+数字) +- [ ] 仪表盘: 管理员最近登录时间、管理员最近登录地区、管理员最近登录IP、提取码使用情况 +- [ ] 支持I18n(EN/CN/JP/KR) + +### 访客端 + +- [ ] 通过指定短链获取文件 +- [ ] 支持I18n(EN/CN/JP/KR) + +## v1.0 + +### 管理端 + +- [x] 管理员登录 / 登出(Session 认证) +- [x] 分享盘(Pan)管理:创建、列表、删除 +- [x] 分享盘详情页:查看关联文件、提取码、权限配置 +- [x] 文件管理:上传文件到分享盘、通过 hash 关联已有文件、从分享盘移除文件 +- [x] 提取码(Code)管理:为分享盘创建唯一提取码 、删除 / 启用/禁用提取码 +- [x] 提取码详情页:查看权限配置 +- [x] 分享盘级权限配置(panPerm):创建、更新、删除(控制默认上传 / 下载能力) +- [x] 提取码 级权限配置(codePerm):创建、更新、删除(覆盖分享盘级权限) + +### 访客端 + +- [x] 通过提取码或 Session 获取分享盘文件列表 +- [x] 文件按 highlight 标记置顶、按更新时间倒序排列 +- [x] 文件下载(基于权限控制) +- [x] 文件上传(基于权限控制):支持全量上传与同 hash 秒传 +- [x] 文件删除(基于权限控制) +- [x] 文件预览:图片、视频内嵌播放,暂不支持类型给出提示 + +### 基础能力 + +- [x] 基于 MD5 hash 的文件去重(同内容只存储一份) +- [x] Cloudflare R2 作为文件存储后端 +- [x] Cloudflare D1(SQLite)作为元数据存储 +- [x] 多层权限模型:Pan 默认权限 + Code 覆盖权限 diff --git a/apps/cf-worker/cloud-storage/components.json b/apps/cf-worker/cloud-storage/components.json new file mode 100644 index 0000000..48c4c70 --- /dev/null +++ b/apps/cf-worker/cloud-storage/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-lyra", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/react-app/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "phosphor", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/apps/cf-worker/cloud-storage/docs/admin_guide.md b/apps/cf-worker/cloud-storage/docs/admin_guide.md new file mode 100644 index 0000000..21e2c83 --- /dev/null +++ b/apps/cf-worker/cloud-storage/docs/admin_guide.md @@ -0,0 +1,77 @@ +# 管理员后台使用指南 + +## 登录 + +1. 访问 `/admin/login` +2. 输入用户名和密码(由环境变量 `USERNAME` / `PASSWORD` 配置) +3. 密码在前端提交前会进行 SHA1 转换再传输 +4. 登录成功后跳转至 `/admin/dashboard`,Cookie 中写入 `user_token` + +--- + +## 仪表盘(`/admin/dashboard`)— 分享盘管理 + +展示所有分享盘列表,字段包括:分享盘ID、状态、最后修改时间、创建时间。 + +### 新增分享盘 + +点击「新增分享盘」按钮,系统自动生成一个新的分享盘记录。 + +### 编辑分享盘 + +点击列表行的「编辑」按钮,跳转至分享盘详情页 `/admin/pans/:id`。 + +### 删除分享盘 + +点击列表行的「删除」按钮,删除该分享盘及其关联数据。 + +--- + +## 分享盘详情(`/admin/pans/:id`) + +### 基础属性 + +| 字段 | 说明 | +|------|------| +| 状态 | 开启 / 关闭。关闭后,该分享盘内所有文件及提取码均无法访问 | + +### 全局权限 + +可为分享盘配置三项默认权限,对所有文件生效: + +| 权限 | 说明 | +|------|------| +| 允许下载文件 | 匿名用户可下载该盘文件 | +| 允许上传文件 | 匿名用户可向该盘上传文件 | +| 允许删除文件 | 匿名用户可删除该盘文件 | + +> 标注「当前为默认权限」表示该权限未通过单独的 Perm 记录覆盖,使用全局默认值。 + +### 文件管理 + +- 支持拖拽或点击上传文件 +- 文件列表字段:文件名、文件大小、是否高亮、最后修改时间、创建时间 +- **高亮**:勾选后该文件在取件页面中会被突出显示 +- 每个文件可编辑(修改文件名 / 高亮状态)或删除 + +### 提取码列表 + +- 点击「新增提取码」为该分享盘生成提取码 +- 提取码列表字段:提取码、状态(启用 / 禁用)、创建时间 +- 「提取地址」按钮:弹窗展示可分享的取件链接 +- 每个提取码可编辑或删除 + +--- + +## 提取码管理(`/admin/codes`) + +展示系统内所有提取码,字段包括:提取码、所属分享盘ID、状态、最后修改时间、创建时间。 + +- 「提取地址」:弹窗展示可分享的取件链接 +- 支持编辑(修改状态)和删除操作 + +--- + +## 登出 + +点击页面右上角「退出登录」按钮:清除 KV 和 Cookie 中的 `user_token`。 diff --git a/apps/cf-worker/cloud-storage/docs/glossary.md b/apps/cf-worker/cloud-storage/docs/glossary.md new file mode 100644 index 0000000..c33c4d4 --- /dev/null +++ b/apps/cf-worker/cloud-storage/docs/glossary.md @@ -0,0 +1,26 @@ +# 名词对照表 + +## 业务名词 + +| 名词 | 中文 | 说明 | +|------|------|------| +| Pan | 分享盘 | 一个独立的文件集合,可关联多个提取码和权限 | +| Code | 提取码 | 匿名用户凭此访问分享盘;可启用 / 禁用 | +| Doc | 文件 | 实际存储的文件元信息(hash、mimetype、size),二进制内容存于 R2 | +| PanDoc | 分享盘文件关联 | Pan 与 Doc 的多对多中间表,包含显示名(originalName)和高亮标志 | +| PanCode | 分享盘提取码关联 | Pan 与 Code 的关联,决定某提取码可访问哪个分享盘 | +| Perm | 权限 | 枚举类型权限项(canDownload / canUpload / canDelete) | +| PanPerm | 分享盘权限 | 为某个 Pan 设置的全局权限 | +| CodePerm | 提取码权限 | 为某个 Code 单独覆盖的权限,优先级高于 PanPerm | +| highlight | 高亮 | PanDoc 字段;高亮文件在取件页面中排列于列表首位并突出展示 | + +## 技术名词 + +| 名词 | 说明 | +|------|------| +| D1 | Cloudflare 托管的 SQLite 数据库,存储所有业务结构化数据 | +| KV | Cloudflare KV 键值存储;存储 session token 及壁纸缓存 | +| R2 | Cloudflare R2 对象存储;存储文件二进制内容,以文件 MD5 hash 为 key | +| `user_token` | 管理员登录后写入 Cookie 的 session token | +| `share_token` | 匿名用户取件后写入 Cookie 的 session token,对应 Code ID | +| ULID | 时间有序唯一 ID,所有数据表主键均使用此格式 | diff --git a/apps/cf-worker/cloud-storage/docs/libraries.md b/apps/cf-worker/cloud-storage/docs/libraries.md new file mode 100644 index 0000000..818163b --- /dev/null +++ b/apps/cf-worker/cloud-storage/docs/libraries.md @@ -0,0 +1,24 @@ +# 库 / 框架文档位置 + +## Worker(后端) + +| 库 / 平台 | 用途 | 文档链接 | +|-----------|------|----------| +| [Hono](https://hono.dev/) | HTTP 框架 | https://hono.dev/docs | +| [Drizzle ORM](https://orm.drizzle.team/) | 数据库 ORM | https://orm.drizzle.team/docs/overview | +| [Cloudflare D1](https://developers.cloudflare.com/d1/) | SQLite 托管数据库 | https://developers.cloudflare.com/d1/ | +| [Cloudflare KV](https://developers.cloudflare.com/kv/) | 键值存储(session / bg 缓存) | https://developers.cloudflare.com/kv/ | +| [Cloudflare R2](https://developers.cloudflare.com/r2/) | 对象存储(文件二进制) | https://developers.cloudflare.com/r2/ | +| [ulid](https://github.com/ulid/spec) | 时间有序唯一 ID 生成 | https://github.com/ulid/javascript | +| [yn](https://github.com/sindresorhus/yn) | 布尔值解析(环境变量 DEBUG) | https://github.com/sindresorhus/yn | + +## React App(前端) + +| 库 / 工具 | 用途 | 文档链接 | +|-----------|------|----------| +| [React](https://react.dev/) | UI 框架 | https://react.dev/reference/react | +| [React Router v7](https://reactrouter.com/) | 客户端路由 | https://reactrouter.com/home | +| [shadcn/ui](https://ui.shadcn.com/) | 无样式 UI 组件集(基于 Radix) | https://ui.shadcn.com/docs | +| [Vite](https://vitejs.dev/) | 构建工具 | https://vitejs.dev/guide/ | +| [Vitest](https://vitest.dev/) | 单元测试 | https://vitest.dev/guide/ | +| [Wrangler](https://developers.cloudflare.com/workers/wrangler/) | Cloudflare Worker 本地开发 / 部署 CLI | https://developers.cloudflare.com/workers/wrangler/ | diff --git a/apps/cf-worker/cloud-storage/docs/pickup_guide.md b/apps/cf-worker/cloud-storage/docs/pickup_guide.md new file mode 100644 index 0000000..387cb15 --- /dev/null +++ b/apps/cf-worker/cloud-storage/docs/pickup_guide.md @@ -0,0 +1,47 @@ +# 提取页面使用说明 + +## 如何获取提取码 + +提取码由管理员在分享盘详情页(`/admin/pans/:id`)的「提取码列表」区块中管理,具体步骤: + +1. 登录管理后台,进入目标分享盘的详情页 +2. 在「提取码列表」区块,点击「新增提取码」生成提取码 +3. 点击提取码对应的「提取地址」按钮,在弹窗中复制可分享的取件链接或提取码 + +--- + +## 取件流程 + +### 1. 输入取件码(`/`) + +1. 打开首页,在输入框中填写提取码 +2. 点击「取件」按钮,跳转至文件列表页 + +### 2. 查看文件列表(`/pan/:code`) + +成功取件后进入文件列表页,展示该分享盘下的所有文件: + +| 字段 | 说明 | +|------|------| +| 文件名 | 文件的原始名称,高亮文件会排列在列表首位 | +| 文件大小 | 文件体积 | +| 上传时间 | 文件上传时间 | + +- 若分享盘开启了**上传权限**,页面右上角显示「上传文件」按钮 +- 点击文件名进入该文件的预览页 + +### 3. 文件预览(`/pan/:code/:hash`) + +进入预览页后: + +- 页面左侧/顶部保留文件列表导航,可直接切换文件 +- 右侧主区域展示文件内容(视频、图片等) +- 若分享盘开启了**下载权限**,页面显示「↓ 下载」按钮,点击即可下载文件 + +--- + +## 上传文件 + +1. 在文件列表页(`/pan/:code`)点击「上传文件」按钮(需分享盘开启上传权限) +2. 选择本地文件后,前端先计算文件的 MD5 +3. 后端校验 MD5:若已存在相同文件则秒传;否则将文件写入 R2 存储并创建关联记录 diff --git a/apps/cf-worker/cloud-storage/docs/project_logic.md b/apps/cf-worker/cloud-storage/docs/project_logic.md new file mode 100644 index 0000000..18790c1 --- /dev/null +++ b/apps/cf-worker/cloud-storage/docs/project_logic.md @@ -0,0 +1,62 @@ +# 项目逻辑 + +## A. 匿名用户 + +### 取件 + +1. 打开页面, 填写取件码 +2. 取件码传递到后端服务 +3. 根据获取到的值, 在Code 中查询 +4. 若查询到值, 则根据Code -> Pan -> Doc 关联关系取得文件列表 +5. 列表数据返回到前端显示 + +#### 下载文件 + +1. 点击列表文件 +2. 如果Pan 的Perm 支持下载, 或者该Doc 的Perm 支持下载, 则在预览页面右上角显示下载按钮 +3. 点击下载按钮进行下载文件 +4. 后端服务将根据Cookie 中的`share_token`, 使用token 作为key 从KV 中找回Code ID +5. 根据Code ID 从Code -> Pan -> Doc 查找, 使用Doc hash, 从R2 中取得文件, 并返回 + +#### 上传文件 + +1. 如果Pan 的Perm 支持上传, 则在列表页面右上角显示上传按钮 +2. 选择文件进行上传, 上传前, 先从文件中获取文件的MD5 +3. 后端服务将根据Cookie 中的`share_token`, 使用token 作为key 从KV 中找回Code ID +4. 根据Code ID 从Code -> Pan 查找 +5. Pan -> Doc 的Doc hash 与传入的MD5 一致时, 则直接完成上传 +6. 若新MD5 时, 则增加Doc, 并将文件传入R2 + +## B. 管理员用户 + +### 登录 + +1. 打开管理员登录页面 +2. 输入用户名及密码, 并传递到后端服务, 其中密码在传输前会进行SHA1 转换 +3. 如果在环境变量填写了`USERNAME`及`PASSWORD`, 则支持登录逻辑操作 +4. 确认用户名及密码无误后, 则在Cookie 加上`user_token` +5. 将token 存入KV + +### 登出 + +1. 在已登录状态下, 可以点击页面登出按钮进行登出 +2. 清除KV 和Cookie 的token + +### 显示分享盘列表 + +1. 在登录状态下, 获取Pan List(可分页) +2. 后端服务获得请求后, 校验`user_token` +3. 从D1 中获取数据 + +### 新增分享盘 + +1. 在登录状态下, 点击新建 +2. 后端服务获得请求后, 校验`user_token` +3. D1 中Doc 增加数据 + +### 删除分享盘 + +1. 在登录状态下, 选择分享盘 +2. 点击删除 +3. 后端服务获得请求后, 校验`user_token` +4. 移除Pan 对应数据, 已经相关联的数据 diff --git a/apps/cf-worker/cloud-storage/docs/project_structure.md b/apps/cf-worker/cloud-storage/docs/project_structure.md new file mode 100644 index 0000000..619def0 --- /dev/null +++ b/apps/cf-worker/cloud-storage/docs/project_structure.md @@ -0,0 +1,22 @@ +# 项目结构 + +``` +- public/ // 存放前端静态文件, 生成后会在根目录下 +- src/ // 存放源码 + |- index.html // 前端入口文件 + |- shared/ + |- |- types/ // 存放共用Typescript 类型文件 + |- react-app/ // 存放前端文件, 使用React 框架 + | |- main.tsx // React 入口文件 + | |- pages/ // 存放页面文件 + | |- components/ // 存放组件文件 + | |- utils/ // 存放工具类文件 + | |- hooks/ // 存放React Hook 类文件 + | |- requests/ // 存放请求类文件 + |- worker/ // 存放Cloudflare worker 源码, 使用Hono 框架 + | |- index.ts // Cloudflare worker 入口文件 + | |- apis/ // 存放对外API 接口文件 + | |- middlewares/ // 存放中间件文件 + | |- utils/ // 存放工具类文件 + | |- models/ // 存放数据库数据表结构文件 +``` diff --git a/apps/cf-worker/cloud-storage/index.html b/apps/cf-worker/cloud-storage/index.html new file mode 100644 index 0000000..58f1e3f --- /dev/null +++ b/apps/cf-worker/cloud-storage/index.html @@ -0,0 +1,13 @@ + + + + + + 乜乜网盘 + + + +
+ + + diff --git a/apps/cf-worker/cloud-storage/package.json b/apps/cf-worker/cloud-storage/package.json new file mode 100644 index 0000000..b0b24bd --- /dev/null +++ b/apps/cf-worker/cloud-storage/package.json @@ -0,0 +1,82 @@ +{ + "name": "@scripts/cloud-storage", + "private": true, + "description": "A template for building a React application with Vite, Hono, and Cloudflare Workers", + "type": "module", + "scripts": { + "cf-typegen": "wrangler types", + "deploy": "npm run build && wrangler deploy", + "dev": "vite --host", + "preview": "npm run build && vite preview", + "clean": "rimraf dist", + "build": "vite build", + "lint": "concurrently -m 1 --timings npm:lint:*", + "lint:type-check": "tsc -p tsconfig.json --noEmit", + "lint:formatter": "oxfmt --check", + "lint:oxlint": "oxlint", + "fix": "concurrently -m 1 --timings npm:fix:*", + "fix:formatter": "oxfmt" + }, + "dependencies": { + "@base-ui/react": "^1.3.0", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@phosphor-icons/react": "^2.1.10", + "@tanstack/react-query": "^5.96.1", + "@videojs/react": "^10.0.0-beta.14", + "axios": "^1.15.0", + "class-variance-authority": "^0.7.1", + "classcat": "^5.0.5", + "clsx": "^2.1.1", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.20", + "drizzle-orm": "^0.45.2", + "hls.js": "^1.6.15", + "hono": "4.11.1", + "lucide-react": "^1.8.0", + "mime-types": "^2.1.35", + "next-themes": "^0.4.6", + "react": "19.2.1", + "react-dom": "19.2.1", + "react-helmet": "^6.1.0", + "react-router": "^7.13.2", + "react-use": "^17.6.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.2", + "ts-pattern": "^5.9.0", + "tw-animate-css": "^1.4.0", + "ulid": "^3.0.2", + "yn": "^5.1.0" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "1.32.2", + "@eslint/js": "9.39.1", + "@tailwindcss/vite": "^4.2.2", + "@types/crypto-js": "^4.2.2", + "@types/mime-types": "^3.0.1", + "@types/node": "^24.12.0", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@types/react-helmet": "^6.1.11", + "@vitejs/plugin-react": "^6.0.1", + "concurrently": "^8.2.2", + "drizzle-kit": "^0.31.10", + "globals": "16.5.0", + "oxfmt": "^0.35.0", + "oxlint": "^1.58.0", + "rimraf": "^6.1.3", + "shadcn": "^4.2.0", + "typescript": "~5.9.3", + "vite": "^8.0.8", + "vitest": "^4.1.4", + "wrangler": "^4.80.0" + }, + "cloudflare": { + "label": "Cloud Storage", + "products": [ + "Workers" + ], + "categories": [], + "publish": true + } +} diff --git a/apps/cf-worker/cloud-storage/public/admin/apple-touch-icon.png b/apps/cf-worker/cloud-storage/public/admin/apple-touch-icon.png new file mode 100644 index 0000000..b443ac1 Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/admin/apple-touch-icon.png differ diff --git a/apps/cf-worker/cloud-storage/public/admin/favicon-96x96.png b/apps/cf-worker/cloud-storage/public/admin/favicon-96x96.png new file mode 100644 index 0000000..f680fdc Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/admin/favicon-96x96.png differ diff --git a/apps/cf-worker/cloud-storage/public/admin/favicon.ico b/apps/cf-worker/cloud-storage/public/admin/favicon.ico new file mode 100644 index 0000000..e436384 Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/admin/favicon.ico differ diff --git a/apps/cf-worker/cloud-storage/public/admin/favicon.svg b/apps/cf-worker/cloud-storage/public/admin/favicon.svg new file mode 100644 index 0000000..85d6232 --- /dev/null +++ b/apps/cf-worker/cloud-storage/public/admin/favicon.svg @@ -0,0 +1 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/apps/cf-worker/cloud-storage/public/admin/site.webmanifest b/apps/cf-worker/cloud-storage/public/admin/site.webmanifest new file mode 100644 index 0000000..9b5cd1f --- /dev/null +++ b/apps/cf-worker/cloud-storage/public/admin/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "MyWebSite", + "short_name": "MySite", + "icons": [ + { + "src": "/admin/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/admin/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/apps/cf-worker/cloud-storage/public/admin/web-app-manifest-192x192.png b/apps/cf-worker/cloud-storage/public/admin/web-app-manifest-192x192.png new file mode 100644 index 0000000..60bdc40 Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/admin/web-app-manifest-192x192.png differ diff --git a/apps/cf-worker/cloud-storage/public/admin/web-app-manifest-512x512.png b/apps/cf-worker/cloud-storage/public/admin/web-app-manifest-512x512.png new file mode 100644 index 0000000..18b1311 Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/admin/web-app-manifest-512x512.png differ diff --git a/apps/cf-worker/cloud-storage/public/apple-touch-icon.png b/apps/cf-worker/cloud-storage/public/apple-touch-icon.png new file mode 100644 index 0000000..fd9ef0c Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/apple-touch-icon.png differ diff --git a/apps/cf-worker/cloud-storage/public/favicon-96x96.png b/apps/cf-worker/cloud-storage/public/favicon-96x96.png new file mode 100644 index 0000000..3b78dc1 Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/favicon-96x96.png differ diff --git a/apps/cf-worker/cloud-storage/public/favicon.ico b/apps/cf-worker/cloud-storage/public/favicon.ico new file mode 100644 index 0000000..707d1f6 Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/favicon.ico differ diff --git a/apps/cf-worker/cloud-storage/public/favicon.svg b/apps/cf-worker/cloud-storage/public/favicon.svg new file mode 100644 index 0000000..57d097f --- /dev/null +++ b/apps/cf-worker/cloud-storage/public/favicon.svg @@ -0,0 +1 @@ +RealFaviconGeneratorhttps://realfavicongenerator.net \ No newline at end of file diff --git a/apps/cf-worker/cloud-storage/public/site.webmanifest b/apps/cf-worker/cloud-storage/public/site.webmanifest new file mode 100644 index 0000000..ac9612c --- /dev/null +++ b/apps/cf-worker/cloud-storage/public/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/apps/cf-worker/cloud-storage/public/web-app-manifest-192x192.png b/apps/cf-worker/cloud-storage/public/web-app-manifest-192x192.png new file mode 100644 index 0000000..a8cbefc Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/web-app-manifest-192x192.png differ diff --git a/apps/cf-worker/cloud-storage/public/web-app-manifest-512x512.png b/apps/cf-worker/cloud-storage/public/web-app-manifest-512x512.png new file mode 100644 index 0000000..279a85f Binary files /dev/null and b/apps/cf-worker/cloud-storage/public/web-app-manifest-512x512.png differ diff --git a/apps/cf-worker/cloud-storage/src/react-app/CLAUDE.md b/apps/cf-worker/cloud-storage/src/react-app/CLAUDE.md new file mode 100644 index 0000000..25052ff --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/CLAUDE.md @@ -0,0 +1,41 @@ +# React App CLAUDE.md + +基于 **React + React Router v7** 的前端 SPA。 + +## 路由结构 + +``` +/ → Home(访客首页,输入 code) +/pan/:code → Detail(文件列表 / 预览) +/pan/:code/:fileHash → Detail(指定文件预览) +/admin/login → Login +/admin/logout → Logout +/admin/session-expired → SessionExpired +/admin/dashboard → AdminPans(等同 /admin/pans) +/admin/pans → AdminPans +/admin/pans/:pan_id → AdminPanDetail(含文件管理) +/admin/pans/:pan_id/codes/:code_id → AdminCodeDetail +/admin/codes → AdminCodes +``` + +所有路由按需懒加载(`React.lazy`)。 + +## 目录说明 + +| 目录 | 说明 | +|------|------| +| `pages/` | 页面组件,按功能分目录 | +| `components/` | 通用组件(Button、Card、TableCell、shadcn/ui) | +| `requests/` | API 请求函数(每个操作独立文件) | +| `hooks/` | 数据获取 hooks(useFileList、usePanInfo、usePanPerms 等) | +| `utils/` | adminFetch / guestFetch 封装、工具函数 | +| `lib/` | 通用工具(cn 等) | + +## 请求工具 + +- `adminFetch` — 自动携带 session,401 时跳转 `/admin/session-expired` +- `guestFetch` — 普通 fetch 封装,用于访客接口 + +## UI 组件 + +使用 **shadcn/ui**,组件存放于 `Components/ui/`。 diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Button/BackButton.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Button/BackButton.tsx new file mode 100644 index 0000000..99968df --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Button/BackButton.tsx @@ -0,0 +1,20 @@ +import cc from 'classcat' +import { lazy, Suspense } from 'react' +import { Button } from '@/components/ui/button' + +const Undo2 = lazy(() => import('lucide-react').then((m) => ({ default: m.Undo2 }))) + +export default function BackButton({ + children = '返回', + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Button/CodeAddressButton.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Button/CodeAddressButton.tsx new file mode 100644 index 0000000..ac24942 --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Button/CodeAddressButton.tsx @@ -0,0 +1,67 @@ +import cc from 'classcat' +import { lazy, Suspense } from 'react' +import { useCopyToClipboard } from 'react-use' +import { toast } from 'sonner' +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' + +const InputGroup = lazy(() => import('../ui/input-group').then((m) => ({ default: m.InputGroup }))) +const InputGroupAddon = lazy(() => + import('../ui/input-group').then((m) => ({ default: m.InputGroupAddon })), +) +const InputGroupInput = lazy(() => + import('../ui/input-group').then((m) => ({ default: m.InputGroupInput })), +) +const Copy = lazy(() => import('lucide-react').then((m) => ({ default: m.Copy }))) +const ExternalLink = lazy(() => import('lucide-react').then((m) => ({ default: m.ExternalLink }))) + +interface Props { + codeValue: string + buttonProps?: React.ComponentProps +} + +export default function CodeAddressButton({ codeValue, buttonProps = {} }: Props) { + const url = `${location.origin}/?code=${codeValue}` + const [, copyToClipboard] = useCopyToClipboard() + + return ( + + + } + > + 提取地址 + + +
+ + + + + { + copyToClipboard(url) + toast.success(`提取码\`${codeValue}\`提取地址已复制到剪贴板`, { + position: 'top-right', + }) + }} + /> + + + window.open(url, '_blank')} + /> + +
+
+
+ ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteButton.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteButton.tsx new file mode 100644 index 0000000..05ffee8 --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteButton.tsx @@ -0,0 +1,90 @@ +import { useMutation } from '@tanstack/react-query' +import { lazy, Suspense, useState } from 'react' +import UseKey from 'react-use/lib/component/UseKey' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' +import { Spinner } from '../ui/spinner' + +interface DeleteButtonProps { + triggerLabel?: string + title?: string + description?: string + confirmLabel?: string + mutationFn: () => Promise + disabled?: boolean + onSuccess?: () => void +} + +const Trash = lazy(() => import('lucide-react').then((m) => ({ default: m.Trash }))) + +export default function DeleteButton({ + triggerLabel = '删除', + title = '确认删除?', + description = '确认要删除吗?此操作不可撤销。', + confirmLabel = '确认删除', + mutationFn, + disabled, + onSuccess, +}: DeleteButtonProps) { + const [open, setOpen] = useState(false) + + const { mutate, isPending } = useMutation({ + mutationFn, + onSuccess: () => { + setOpen(false) + onSuccess?.() + }, + }) + + return ( + <> + + + + + {title} + {description} + + + + {open && !isPending && setOpen(false)} />} + 取消 + + mutate()} + > + {isPending && } + {confirmLabel} + + + + + + ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteCodeButton.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteCodeButton.tsx new file mode 100644 index 0000000..256aafc --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteCodeButton.tsx @@ -0,0 +1,25 @@ +import { deleteAdminCode } from '@/requests/fetchAdminCodes' +import DeleteButton from './DeleteButton' + +interface DeleteCodeButtonProps { + panId: string + codeId: string + disabled?: boolean + onSuccess?: () => void +} + +export default function DeleteCodeButton({ + panId, + codeId, + disabled, + onSuccess, +}: DeleteCodeButtonProps) { + return ( + deleteAdminCode(panId, codeId)} + disabled={disabled} + onSuccess={onSuccess} + /> + ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteFileButton.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteFileButton.tsx new file mode 100644 index 0000000..6933c9e --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeleteFileButton.tsx @@ -0,0 +1,25 @@ +import { deleteAdminPanFile } from '@/requests/fetchAdminPanDetail' +import DeleteButton from './DeleteButton' + +interface DeleteFileButtonProps { + panId: string + fileHash: string + disabled?: boolean + onSuccess?: () => void +} + +export default function DeleteFileButton({ + panId, + fileHash, + disabled, + onSuccess, +}: DeleteFileButtonProps) { + return ( + deleteAdminPanFile(panId, fileHash)} + disabled={disabled} + onSuccess={onSuccess} + /> + ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeletePanButton.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeletePanButton.tsx new file mode 100644 index 0000000..ef3de89 --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Button/DeletePanButton.tsx @@ -0,0 +1,19 @@ +import { deleteAdminPan } from '@/requests/fetchAdminPans' +import DeleteButton from './DeleteButton' + +interface DeletePanButtonProps { + panId: string + disabled?: boolean + onSuccess?: () => void +} + +export default function DeletePanButton({ panId, disabled, onSuccess }: DeletePanButtonProps) { + return ( + deleteAdminPan(panId)} + disabled={disabled} + onSuccess={onSuccess} + /> + ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Button/EditButton.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Button/EditButton.tsx new file mode 100644 index 0000000..d5420e8 --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Button/EditButton.tsx @@ -0,0 +1,27 @@ +import cc from 'classcat' +import { lazy, Suspense } from 'react' +import { Button } from '@/components/ui/button' + +const Pencil = lazy(() => import('lucide-react').then((m) => ({ default: m.Pencil }))) + +export default function EditButton({ + children = '编辑', + ...props +}: React.ComponentProps) { + return ( + + ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/Card/Card.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/Card/Card.tsx new file mode 100644 index 0000000..d44fcef --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/Card/Card.tsx @@ -0,0 +1,13 @@ +import cc from 'classcat' +import { HTMLAttributes, PropsWithChildren } from 'react' + +export default function Card(props: PropsWithChildren>) { + return ( + <> +
+ + ) +} diff --git a/apps/cf-worker/cloud-storage/src/react-app/components/ui/alert-dialog.tsx b/apps/cf-worker/cloud-storage/src/react-app/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..f43d3ae --- /dev/null +++ b/apps/cf-worker/cloud-storage/src/react-app/components/ui/alert-dialog.tsx @@ -0,0 +1,159 @@ +import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog' +import * as React from 'react' +import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { + return +} + +function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { + return +} + +function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { + return +} + +function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = 'default', + ...props +}: AlertDialogPrimitive.Popup.Props & { + size?: 'default' | 'sm' +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ className, ...props }: React.ComponentProps) { + return