From c18d70017ed1045124330a2891e22f009255bb36 Mon Sep 17 00:00:00 2001 From: R524809 Date: Fri, 14 Aug 2026 18:27:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=80=E5=8F=91=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 7 +- .gitignore | 1 + README.md | 26 +- docs/architecture.md | 131 +- docs/studio/image-edit.md | 71 + server/api/image.py | 10 +- server/config/settings.py | 6 +- server/requirements.txt | 1 + server/schemas/image_edit.py | 125 + server/services/image_edit.py | 185 ++ start.command | 4 +- studio/.env.example | 4 + studio/.gitignore | 5 + studio/index.html | 12 + studio/package.json | 28 + studio/pnpm-lock.yaml | 2043 +++++++++++++++++ .../imgs/panda_shop_200x200_opacity30.png | Bin 0 -> 84296 bytes studio/public/imgs/watermark.jpg | Bin 0 -> 14372 bytes studio/src/App.css | 1 + studio/src/App.tsx | 25 + studio/src/config/env.ts | 20 + studio/src/index.css | 35 + studio/src/layouts/MainLayout.css | 115 + studio/src/layouts/MainLayout.tsx | 102 + studio/src/layouts/SidebarMenu.css | 82 + studio/src/layouts/SidebarMenu.tsx | 61 + studio/src/layouts/menuConfig.tsx | 39 + studio/src/main.tsx | 10 + studio/src/pages/ai-image/AiImagePage.css | 538 +++++ studio/src/pages/ai-image/AiImagePage.tsx | 266 +++ .../ai-image/components/AnnotationCanvas.tsx | 191 ++ .../ai-image/components/ElementPropsPanel.tsx | 166 ++ .../ai-image/components/ImageEditModal.tsx | 479 ++++ .../ai-image/components/WatermarkCanvas.tsx | 99 + studio/src/pages/ai-image/index.ts | 1 + studio/src/router/index.tsx | 15 + studio/src/services/api.ts | 48 + studio/src/services/image.ts | 7 + studio/src/types/annotation.ts | 43 + studio/src/types/image.ts | 57 + studio/src/utils/annotation.ts | 233 ++ studio/src/utils/image.ts | 37 + studio/src/utils/watermark.ts | 145 ++ studio/src/vite-env.d.ts | 1 + studio/tsconfig.app.json | 26 + studio/tsconfig.app.tsbuildinfo | 1 + studio/tsconfig.json | 7 + studio/tsconfig.node.json | 19 + studio/tsconfig.node.tsbuildinfo | 1 + studio/vite.config.ts | 24 + 50 files changed, 5496 insertions(+), 57 deletions(-) create mode 100644 docs/studio/image-edit.md create mode 100644 server/schemas/image_edit.py create mode 100644 server/services/image_edit.py create mode 100644 studio/.env.example create mode 100644 studio/.gitignore create mode 100644 studio/index.html create mode 100644 studio/package.json create mode 100644 studio/pnpm-lock.yaml create mode 100644 studio/public/imgs/panda_shop_200x200_opacity30.png create mode 100644 studio/public/imgs/watermark.jpg create mode 100644 studio/src/App.css create mode 100644 studio/src/App.tsx create mode 100644 studio/src/config/env.ts create mode 100644 studio/src/index.css create mode 100644 studio/src/layouts/MainLayout.css create mode 100644 studio/src/layouts/MainLayout.tsx create mode 100644 studio/src/layouts/SidebarMenu.css create mode 100644 studio/src/layouts/SidebarMenu.tsx create mode 100644 studio/src/layouts/menuConfig.tsx create mode 100644 studio/src/main.tsx create mode 100644 studio/src/pages/ai-image/AiImagePage.css create mode 100644 studio/src/pages/ai-image/AiImagePage.tsx create mode 100644 studio/src/pages/ai-image/components/AnnotationCanvas.tsx create mode 100644 studio/src/pages/ai-image/components/ElementPropsPanel.tsx create mode 100644 studio/src/pages/ai-image/components/ImageEditModal.tsx create mode 100644 studio/src/pages/ai-image/components/WatermarkCanvas.tsx create mode 100644 studio/src/pages/ai-image/index.ts create mode 100644 studio/src/router/index.tsx create mode 100644 studio/src/services/api.ts create mode 100644 studio/src/services/image.ts create mode 100644 studio/src/types/annotation.ts create mode 100644 studio/src/types/image.ts create mode 100644 studio/src/utils/annotation.ts create mode 100644 studio/src/utils/image.ts create mode 100644 studio/src/utils/watermark.ts create mode 100644 studio/src/vite-env.d.ts create mode 100644 studio/tsconfig.app.json create mode 100644 studio/tsconfig.app.tsbuildinfo create mode 100644 studio/tsconfig.json create mode 100644 studio/tsconfig.node.json create mode 100644 studio/tsconfig.node.tsbuildinfo create mode 100644 studio/vite.config.ts diff --git a/.env.example b/.env.example index 5a1e2be..df583d6 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,12 @@ DEEPSEEK_API_KEY=sk-xxxx # 以后若接入其他厂商,按 models.yaml 中的 api_key_env 增加对应变量,例如: # OPENAI_API_KEY=sk-xxxx +# 阿里云百炼(万相图生图 wanx2.1-imageedit),华北2(北京)地域的 API Key +DASHSCOPE_API_KEY=sk-xxxx +# 仅业务空间调用时填写:https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1 +# DASHSCOPE_BASE_HTTP_API_URL= + HOST=127.0.0.1 -PORT=8000 +PORT=8800 # Comma-separated origins when frontend runs on another port. Same-origin mount can leave empty. CORS_ORIGINS= diff --git a/.gitignore b/.gitignore index 9ba40ec..9edea78 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ node_modules/ dist/ .output/ .wxt/ +.pnpm-store/ diff --git a/README.md b/README.md index 5ab07ec..91e0551 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Ozon 跨境上品工具链:**采集 → 编辑 → 发布**。 |---|---|---| | `web/` | ① 工具台 v1:计价、登记、水印、俄文文案 | ✅ 在用(冻结) | | `extension/` | ② Chrome 采集插件:Ozon / 1688 商品页采集 | 🔨 待开发 | -| `studio/` | ③ 发布工作台:导入文件夹 → 编辑 → 发布 | 📋 待设计 | -| `server/` | ④ FastAPI:AI 文案、图片处理、Ozon API | 🔨 部分就绪 | +| `studio/` | ③ 发布工作台:AI 图生图(上传/水印/万相图生图) | 🔨 开发中 | +| `server/` | ④ FastAPI:AI 文案、图生图(wanx2.1-imageedit)、Ozon API | 🔨 部分就绪 | 四者通过磁盘上的「[商品文件夹](docs/contracts/product-json.md)」契约衔接,不直接耦合代码。 @@ -20,7 +20,7 @@ ozon-seller-kit/ ├── server/ # ④ FastAPI(main.py / api / services / schemas / config) ├── web/ # ① 工具台 v1,冻结 ├── extension/ # ② 采集插件(待建) -├── studio/ # ③ 发布工作台(待建) +├── studio/ # ③ 发布工作台(React + Vite + antd,AI 图生图已上线) ├── packages/schema/ # 跨端共享契约(待建) ├── docs/ # 全部文档 ├── start.command @@ -35,11 +35,28 @@ ozon-seller-kit/ ./start.command ``` -打开 http://127.0.0.1:8000/ozonSeller.html +打开 http://127.0.0.1:8800/ozonSeller.html - 密钥写在根目录 `.env`(参考 `.env.example`) - 可选模型写在 `server/config/models.yaml` +### 发布工作台(studio/,AI 图生图) + +先启动后端(同上 `./start.command`,或确保 `uvicorn` 跑在 8800),再启动前端: + +```bash +cd studio +pnpm install +pnpm dev # http://localhost:8900(启动后自动打开浏览器) +``` + +前端通过 Vite 代理把 `/api` 转发到 `http://127.0.0.1:8800`。图生图依赖阿里云百炼的 +**wanx2.1-imageedit**(华北2/北京),需在 `.env` 配置: + +```bash +DASHSCOPE_API_KEY=sk-xxxx +``` + ## 文档 | 文档 | 内容 | @@ -50,3 +67,4 @@ ozon-seller-kit/ | [`docs/extension/plan-revision.md`](docs/extension/plan-revision.md) | 插件方案修正(针对 Ozon 优先)| | [`docs/deployment.md`](docs/deployment.md) | 部署与启动 | | [`docs/ai-copy-backend-plan.md`](docs/ai-copy-backend-plan.md) | 俄文文案后端方案 | +| [`docs/studio/image-edit.md`](docs/studio/image-edit.md) | AI 图生图(studio 页面 + /api/image/edit) | diff --git a/docs/architecture.md b/docs/architecture.md index 7c58d8d..73628f7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,17 +12,21 @@ 四个组成部分,各自独立可用、通过明确契约衔接: -| # | 部分 | 形态 | 状态 | 职责 | -|---|---|---|---|---| -| ① | **工具台 v1** | 静态页(原生 JS + Tailwind CDN) | ✅ 在用 | 计价、上品登记、水印、俄文文案。**冻结维护,不重构** | -| ② | **采集插件** | Chrome MV3 扩展 | 🔨 待开发 | Ozon/1688 商品页采集 → 落本地文件夹 | -| ③ | **发布工作台** | Web 应用 | 📋 待设计 | 导入本地文件夹 → 编辑/图片处理 → 提交发布 | -| ④ | **服务端** | FastAPI | 🔨 部分就绪 | AI 文案、图片处理、Ozon API 代理、凭证保管 | + +| # | 部分 | 形态 | 状态 | 职责 | +| --- | ---------- | ------------------------- | ------- | ---------------------------- | +| ① | **工具台 v1** | 静态页(原生 JS + Tailwind CDN) | ✅ 在用 | 计价、上品登记、水印、俄文文案。**冻结维护,不重构** | +| ② | **采集插件** | Chrome MV3 扩展 | 🔨 待开发 | Ozon/1688 商品页采集 → 落本地文件夹 | +| ③ | **发布工作台** | Web 应用 | 📋 待设计 | 导入本地文件夹 → 编辑/图片处理 → 提交发布 | +| ④ | **服务端** | FastAPI | 🔨 部分就绪 | AI 文案、图片处理、Ozon API 代理、凭证保管 | + **关键边界**:① 与 ③ 并存不互相替代。① 是已验证好用的轻量工具,③ 是面向"整商品发布"的新流程;③ 成熟后 ① 的计价器可能被吸收,但那是以后的事。 --- + + ## 2. 目录结构 ``` @@ -74,7 +78,7 @@ ozon-seller-kit/ 代价:`start.command` 与 `docs/deployment.md` 里的启动路径要改,`.env` 加载路径 `parents[1]` 要跟着调。Python 内部 import 全是 `from api import ...` 这类顶层相对形式,只要工作目录切到 `server/` 就不受影响。 -**这是唯一有破坏性的改动,建议在动 `studio/` 之前一次做完,不要拖到中途。** 如果你想零风险,也可以让后端留在根目录——架构其余部分不依赖这个决定。 +**这是唯一有破坏性的改动,建议在动** `studio/` **之前一次做完,不要拖到中途。** 如果你想零风险,也可以让后端留在根目录——架构其余部分不依赖这个决定。 ### 2.2 为什么 `web/` 不改名 @@ -84,6 +88,8 @@ ozon-seller-kit/ --- + + ## 3. 核心数据契约:商品文件夹 **这是整个架构最重要的一个决定。** 四个部分之间不直接调用彼此的代码,只认一个约定:磁盘上的「商品文件夹」。 @@ -113,23 +119,27 @@ ozon-seller-kit/ 2. **中间产物可见可改**。采集结果就是普通文件夹,能用 Finder 看、能手动补图、能备份、能在两台机器间拷。 3. **跨平台合并天然成立**。Ozon 采完,切到 1688 采同类商品,选同一个文件夹继续写入——就是往同一目录追加文件,不需要任何服务端参与。 + + ### 3.1 product.json 结构对齐 Ozon `ProductAPI_ImportProductsV3`,但**不等于**它的请求体。区别在于采集阶段拿不到的字段留空,由工作台补齐: -| 字段 | 采集阶段 | 工作台补齐 | -|---|---|---| -| `name` / `description` | ✅ 原文(可能是俄文) | 改写/翻译 | -| `images` | 本地相对路径 | 上传图床后换成公网 URL | -| `offer_id` | ❌ 空 | **必须填自己的货号**(跟卖场景下不能沿用竞品的) | -| `description_category_id` | ❌ 空 | 类目选择/推荐 | -| `attributes[].id` | ❌ 空 | 查类目属性字典映射 | -| 尺寸重量 | 参数表里有就带上 | 校验补全 | -| `price` / `old_price` | 采到竞品价,仅供参考 | 计价器算出 | + +| 字段 | 采集阶段 | 工作台补齐 | +| ------------------------- | ----------- | -------------------------- | +| `name` / `description` | ✅ 原文(可能是俄文) | 改写/翻译 | +| `images` | 本地相对路径 | 上传图床后换成公网 URL | +| `offer_id` | ❌ 空 | **必须填自己的货号**(跟卖场景下不能沿用竞品的) | +| `description_category_id` | ❌ 空 | 类目选择/推荐 | +| `attributes[].id` | ❌ 空 | 查类目属性字典映射 | +| 尺寸重量 | 参数表里有就带上 | 校验补全 | +| `price` / `old_price` | 采到竞品价,仅供参考 | 计价器算出 | + 所以 product.json 有个 `_meta.stage` 字段标明它处在哪个阶段:`collected` → `edited` → `published`。 -契约细节见 [`docs/contracts/product-json.md`](./contracts/product-json.md)。 +契约细节见 `[docs/contracts/product-json.md](./contracts/product-json.md)`。 ### 3.2 契约真源与双语言实现 @@ -148,8 +158,12 @@ server/schemas/product.py (Pydantic) --- + + ## 4. 各部分职责与边界 + + ### ① 工具台 v1(`web/`) **冻结。** 只修 bug,不加功能、不重构、不迁技术栈。它当前的价值是「已经好用」,任何改动都是风险。 @@ -162,7 +176,7 @@ server/schemas/product.py (Pydantic) 不做:LLM 调用、图片处理、Ozon API、持有任何密钥。 -一期只支持 Ozon(跟卖是第一优先级),1688 二期。详见 [`docs/extension/plan.md`](./extension/plan.md)。 +一期只支持 Ozon(跟卖是第一优先级),1688 二期。详见 `[docs/extension/plan.md](./extension/plan.md)`。 ### ③ 发布工作台(`studio/`) @@ -172,6 +186,8 @@ server/schemas/product.py (Pydantic) - **图片处理**:水印、白底、图内翻译、图生图 —— 重活走服务端 - **发布**:提交 Ozon 草稿,回填 `product_id` + + ### ④ 服务端(`server/`) 唯一持有密钥的地方。当前有 `/api/ai/*`(文案);待补 `/api/image/*`、`/api/ozon/*`。 @@ -180,6 +196,8 @@ server/schemas/product.py (Pydantic) --- + + ## 5. 端到端数据流 ``` @@ -204,38 +222,50 @@ server/schemas/product.py (Pydantic) --- + + ## 6. 技术栈 -| 部分 | 技术栈 | 说明 | -|---|---|---| -| ① web | 原生 JS + Tailwind CDN | 不动 | -| ② extension | WXT + React + TS strict | WXT 比 Plasmo 活跃 | -| ③ studio | 待定(见 §8) | | -| ④ server | FastAPI + Pydantic | 已有 | -| 共享 | pnpm workspace | 只为 `packages/schema` 共享,不上 turborepo | + +| 部分 | 技术栈 | 说明 | +| ----------- | ----------------------- | ------------------------------------ | +| ① web | 原生 JS + Tailwind CDN | 不动 | +| ② extension | WXT + React + TS strict | WXT 比 Plasmo 活跃 | +| ③ studio | 待定(见 §8) | | +| ④ server | FastAPI + Pydantic | 已有 | +| 共享 | pnpm workspace | 只为 `packages/schema` 共享,不上 turborepo | + pnpm workspace 的唯一目的是让插件和 studio 共用契约类型。`pnpm-workspace.yaml` 三行搞定,不引入构建编排复杂度。 --- + + ## 7. 服务端演进 当前是无状态的:只有 AI 文案代理,没有数据库。按需要逐步加,不要一次上齐: -| 阶段 | 触发条件 | 要加什么 | -|---|---|---| -| 现在 | — | 无状态,`/api/ai/*` | -| S1 | studio 要处理图片 | `/api/image/*`(水印/白底/翻译),仍无状态:收图返图 | -| S2 | 要发布到 Ozon | `/api/ozon/*` + 图床 + 类目字典缓存(SQLite 够用) | -| S3 | 图片处理变慢(图生图、视频) | 任务队列 + `/api/job/:id` 轮询 | -| S4 | 想要跨设备同步 | 商品库落库,本地文件夹降级为导入导出格式 | + +| 阶段 | 触发条件 | 要加什么 | +| --- | -------------- | -------------------------------------- | +| 现在 | — | 无状态,`/api/ai/*` | +| S1 | studio 要处理图片 | `/api/image/*`(水印/白底/翻译),仍无状态:收图返图 | +| S2 | 要发布到 Ozon | `/api/ozon/*` + 图床 + 类目字典缓存(SQLite 够用) | +| S3 | 图片处理变慢(图生图、视频) | 任务队列 + `/api/job/:id` 轮询 | +| S4 | 想要跨设备同步 | 商品库落库,本地文件夹降级为导入导出格式 | + **S1、S2 都不需要数据库**,类目字典用文件缓存即可。S4 是个大改动,只在真的有多设备需求时才做——本地文件夹方案的一个优点就是单机场景下完全不需要它。 --- + + ## 8. 已定决策 + + ### D1 · 后端迁入 `server/` ✅ 代价是改 `start.command`、`settings.py` 的 `.env` 路径、`deployment.md`。Python 内部 import 不受影响(工作目录切到 `server/` 即可)。**这是唯一有破坏性的改动,在开工 studio 前一次做完。** @@ -250,10 +280,12 @@ studio 的核心是"几十个字段的结构化表单 + 图片批处理",正 File System Access 在 side panel(扩展页面上下文)可用,`showDirectoryPicker()` 拿到的 handle 存进 IndexedDB 后**跨会话免重复授权**,正好支撑"Ozon 采完切 1688 追加到同一文件夹"。`chrome.downloads` 保留为降级路径(用户拒绝授权时)。 -细节见 [`docs/extension/plan-revision.md`](./extension/plan-revision.md) R1。 +细节见 `[docs/extension/plan-revision.md](./extension/plan-revision.md)` R1。 --- + + ## 9. 迁移计划 一次性做完,中途不要停在半路: @@ -285,26 +317,29 @@ File System Access 在 side panel(扩展页面上下文)可用,`showDirect ⑥ web/ 加 README 标注 v1 冻结 ``` -`reference/1688-extension/` 要不要进 git:它是反编译产物,约 40 个 bundle。建议**加进 `.gitignore`**,保留在本地即可——设计结论已经写进 `docs/extension/plan.md` §2,原始 bundle 只在需要再次查证时才用。 +`reference/1688-extension/` 要不要进 git:它是反编译产物,约 40 个 bundle。建议**加进** `.gitignore`,保留在本地即可——设计结论已经写进 `docs/extension/plan.md` §2,原始 bundle 只在需要再次查证时才用。 --- + + ## 10. 里程碑(已调整优先级) -**插件先行:1688/淘宝 → Ozon**。理由见 [`docs/extension/1688-taobao-implementation.md`](./extension/1688-taobao-implementation.md)。 +**插件先行:1688/淘宝 → Ozon**。理由见 `[docs/extension/1688-taobao-implementation.md](./extension/1688-taobao-implementation.md)`。 -| # | 内容 | 依赖 | 产出 | 工作量 | -|---|---|---|---|---| -| **M0** | 目录迁移 + 文档集中 | — | ✅ 已完成,服务正常起 | — | -| **M1** | 插件:1688 采集引擎 | M0 | Console 里能跑 `scanCurrentPage()` | 4h | -| **M2** | 插件:淘宝 profile | M1 | 淘宝页面同样可用 | 1h | -| **M3** | 插件:Side Panel + File System Access | M2 | 生成完整商品文件夹到本地 | 4h | -| **M4** | 插件:sources.json 去重 | M3 | 二次采集追加不重复 | 1h | -| **M5** | 契约真源:product.json Pydantic | M0 | `server/schemas/product.py` | 2h | -| **M6** | 插件:Ozon profile 实测 | M4 | Ozon 选择器验证(需真实页面链接) | 2h | -| **M7** | studio:导入文件夹 + 表单编辑 | M5 | 能改能存 | 8h | -| **M8** | studio + server:图片处理 | M7 | 水印/白底可用 | 6h | -| **M9** | server:Ozon 发布 | M8 | 草稿进 Ozon 后台 | 4h | -**M4 结束时插件功能完整**,可以采集 1688/淘宝商品到本地文件夹,跨平台追加不重复。M9 结束时全链路跑通。 +| # | 内容 | 依赖 | 产出 | 工作量 | +| ------ | ---------------------------------- | --- | ------------------------------- | --- | +| **M0** | 目录迁移 + 文档集中 | — | ✅ 已完成,服务正常起 | — | +| **M1** | 插件:1688 采集引擎 | M0 | Console 里能跑 `scanCurrentPage()` | 4h | +| **M2** | 插件:淘宝 profile | M1 | 淘宝页面同样可用 | 1h | +| **M3** | 插件:Side Panel + File System Access | M2 | 生成完整商品文件夹到本地 | 4h | +| **M4** | 插件:sources.json 去重 | M3 | 二次采集追加不重复 | 1h | +| **M5** | 契约真源:product.json Pydantic | M0 | `server/schemas/product.py` | 2h | +| **M6** | 插件:Ozon profile 实测 | M4 | Ozon 选择器验证(需真实页面链接) | 2h | +| **M7** | studio:导入文件夹 + 表单编辑 | M5 | 能改能存 | 8h | +| **M8** | studio + server:图片处理 | M7 | 水印/白底可用 | 6h | +| **M9** | server:Ozon 发布 | M8 | 草稿进 Ozon 后台 | 4h | + +**M4 结束时插件功能完整**,可以采集 1688/淘宝商品到本地文件夹,跨平台追加不重复。M9 结束时全链路跑通。 \ No newline at end of file diff --git a/docs/studio/image-edit.md b/docs/studio/image-edit.md new file mode 100644 index 0000000..bb8ca6a --- /dev/null +++ b/docs/studio/image-edit.md @@ -0,0 +1,71 @@ +# AI 图生图(wanx2.1-imageedit) + +「发布工作台」(`studio/`)当前唯一的页面:上传多张图片 → 加水印(文字/图片)→ 对单张图片调用 +万相 2.1 通用图像编辑模型做图生图。 + +## 目录与职责 + +| 位置 | 职责 | +|---|---| +| `studio/src/pages/ai-image/` | 页面:上传、水印设置、预览网格、AI 弹窗 | +| `studio/src/pages/ai-image/components/WatermarkCanvas.tsx` | 单图可拖拽水印画布 | +| `studio/src/pages/ai-image/components/ImageEditModal.tsx` | 图生图弹窗(上:原图/生成对比;下:对话框) | +| `studio/src/utils/watermark.ts` | 水印绘制(移植自 `web/js/app.js`,坐标相对原图) | +| `studio/src/services/image.ts` | 前端调用后端 `/api/image/edit` | +| `server/api/image.py` | 路由 `POST /api/image/edit` | +| `server/services/image_edit.py` | 万相模型调用(同步 SDK 在线程池执行) | +| `server/schemas/image_edit.py` | 请求/响应 Pydantic 模型 | + +## 接口契约 + +`POST /api/image/edit` + +请求(JSON): + +```json +{ + "base_image": "data:image/png;base64,... | https://...", + "prompt": "把背景换成纯白色摄影棚", + "function": "description_edit", + "n": 1, + "size": "", + "seed": null, + "style": "", + "prompt_extend": true, + "strength": 0.8 +} +``` + +- `base_image`:前端把「已加水印」的当前图片以 data URL 传入(data URL 与公网 URL 二选一)。 +- `function` 白名单:`description_edit`(文本指令编辑)、`description_edit_with_mask`(局部重绘,需 `mask_image`)、 + `stylization_local`、`stylization_all`。默认 `description_edit`。 +- `prompt_extend`:是否自动扩写提示词,官方示例默认开启(默认 `true`)。 +- `strength`:修改幅度 0.0~1.0,官方默认 0.5(值越小越接近原图)。 + **新增文字、大幅修改等场景建议调高到 0.8 左右**,否则模型可能因"贴近原图"而忽略新内容。 + +响应: + +```json +{ + "task_id": "6e319d88-...", + "results": [{ "url": "https://...oss-cn-wulanchabu...png?...", "image_base64": "data:image/png;base64,..." }], + "image_count": 1, + "request_id": "dc41682c-..." +} +``` + +> `url` 有效期 24 小时。`image_base64` 为服务端代下载的 data URL,前端可据此直接合成/导出, +> 规避阿里云结果 URL 未开放 CORS 导致的画布污染(无法 toDataURL)。 + +## 密钥与地域 + +- 模型:`wanx2.1-imageedit`,仅支持**华北 2(北京)**地域的 API Key。 +- 密钥:`.env` 中的 `DASHSCOPE_API_KEY`。 +- 业务空间调用时,另配 `DASHSCOPE_BASE_HTTP_API_URL=https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1`; + 普通 API Key 调用留空。 + +## 关于图片存储 + +当前服务端**不做图片存储**:前端直接把 base64 图片传给模型,结果 URL 原样返回给前端展示。 +后续接入七牛云时,可在 `services/image_edit.py` 拿到 `results[].url` 后增加「下载 → 转存七牛 → 返回新 URL」一步, +api 层与前端契约无需改动。 diff --git a/server/api/image.py b/server/api/image.py index 35fb140..adc5278 100644 --- a/server/api/image.py +++ b/server/api/image.py @@ -1,5 +1,13 @@ from fastapi import APIRouter +from schemas.image_edit import ImageEditRequest, ImageEditResponse +from services.image_edit import edit_image + router = APIRouter(prefix="/api/image", tags=["image"]) -# Phase 2: watermark / white background / img2img proxy + +@router.post("/edit", response_model=ImageEditResponse, summary="AI 图生图(wanx2.1-imageedit)") +async def create_image_edit(body: ImageEditRequest) -> ImageEditResponse: + return await edit_image(body) + +# 后续扩展点:白底 / 水印等服务端处理可继续在此文件下新增路由。 diff --git a/server/config/settings.py b/server/config/settings.py index ef9e7e1..34569cb 100644 --- a/server/config/settings.py +++ b/server/config/settings.py @@ -20,8 +20,12 @@ class Settings(BaseSettings): deepseek_api_key: str = "" openai_api_key: str = "" + dashscope_api_key: str = "" + # 华北2(北京)业务空间时需填:https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1 + # 普通 API Key 调用留空即可。 + dashscope_base_http_api_url: str = "" host: str = "127.0.0.1" - port: int = 8000 + port: int = 8800 cors_origins: str = "" @property diff --git a/server/requirements.txt b/server/requirements.txt index 33c378b..10361c4 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -4,3 +4,4 @@ httpx>=0.27.0 pydantic-settings>=2.6.0 python-dotenv>=1.0.0 PyYAML>=6.0.0 +dashscope>=1.23.8 diff --git a/server/schemas/image_edit.py b/server/schemas/image_edit.py new file mode 100644 index 0000000..c69750d --- /dev/null +++ b/server/schemas/image_edit.py @@ -0,0 +1,125 @@ +"""图生图 / 图像编辑请求响应模型(多模型)。""" +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, field_validator + +# 支持的模型白名单 +SUPPORTED_MODELS: frozenset[str] = frozenset( + { + "wanx2.1-imageedit", + "wan2.6-image", + "qwen-image-edit", + "qwen-image-edit-plus", + "qwen-image-edit-plus-2025-10-30", + } +) + +# 万相 2.1 通用图像编辑支持的 function 白名单 +# description_edit :文本指令编辑(无掩码,即“图生图”) +# description_edit_with_mask :局部重绘(需 mask) +# stylization_local :局部风格化 +# stylization_all :整体风格化 +ImageEditFunction = Literal[ + "description_edit", + "description_edit_with_mask", + "stylization_local", + "stylization_all", +] + +# 单张 base64 图片(编码后)上限,超出直接拒绝,避免无谓占用内存。 +_MAX_BASE64_LENGTH = 15 * 1024 * 1024 + + +class ImageEditRequest(BaseModel): + """图生图请求。图片以 data URL(data:image/...;base64,...)或公网 URL 传入。""" + + base_image: str = Field(..., description="待编辑原图:data URL 或公网 http(s) URL") + prompt: str = Field(..., description="编辑指令,例如“把背景换成纯白色摄影棚”") + model: str = Field( + default="wanx2.1-imageedit", + description="模型 code,见 SUPPORTED_MODELS", + ) + mask_image: str | None = Field( + default=None, description="掩码图(仅 function=description_edit_with_mask 时使用)" + ) + function: ImageEditFunction = Field( + default="description_edit", description="编辑能力类型,见 ImageEditFunction" + ) + n: int = Field(default=1, ge=1, le=4, description="生成数量") + size: str = Field(default="", description="输出尺寸,如 1024*1024;留空由模型决定") + seed: int | None = Field(default=None, ge=0, description="随机种子,可选") + style: str = Field(default="", description="输出风格,如 auto/photography/anime 等,可选") + prompt_extend: bool = Field(default=True, description="是否自动扩写提示词(官方示例默认开启)") + strength: float | None = Field( + default=None, + ge=0.0, + le=1.0, + description=( + "修改幅度 0.0~1.0,官方默认 0.5;值越大改动越明显、越偏离原图。" + "新增文字、大幅修改等场景建议 0.8 左右" + ), + ) + + @field_validator("prompt") + @classmethod + def prompt_not_empty(cls, value: str) -> str: + text = (value or "").strip() + if not text: + raise ValueError("prompt 不能为空") + return text + + @field_validator("model") + @classmethod + def model_whitelist(cls, value: str) -> str: + model = (value or "").strip() + if model not in SUPPORTED_MODELS: + raise ValueError(f"不支持的模型「{model}」,可选:{sorted(SUPPORTED_MODELS)}") + return model + + @field_validator("base_image") + @classmethod + def base_image_valid(cls, value: str) -> str: + image = (value or "").strip() + if not image: + raise ValueError("base_image 不能为空") + if image.startswith("data:image/"): + if len(image) > _MAX_BASE64_LENGTH: + raise ValueError("图片过大,请压缩后重试") + elif not image.startswith(("http://", "https://")): + raise ValueError("base_image 仅支持 data URL 或公网 http(s) URL") + return image + + @field_validator("mask_image") + @classmethod + def mask_image_valid(cls, value: str | None) -> str | None: + if value is None or not value.strip(): + return None + image = value.strip() + if image.startswith("data:image/"): + if len(image) > _MAX_BASE64_LENGTH: + raise ValueError("mask_image 图片过大") + elif not image.startswith(("http://", "https://")): + raise ValueError("mask_image 仅支持 data URL 或公网 http(s) URL") + return image + + @field_validator("style") + @classmethod + def normalize_style(cls, value: str) -> str: + return (value or "").strip() + + +class ImageEditResult(BaseModel): + url: str = Field(..., description="生成图片 URL(有效期 24 小时)") + image_base64: str = Field( + default="", + description="生成图片的 data URL(服务端代理下载,便于前端直接合成/导出,规避跨域画布污染)", + ) + + +class ImageEditResponse(BaseModel): + task_id: str = "" + results: list[ImageEditResult] = Field(default_factory=list) + image_count: int = 0 + request_id: str = "" diff --git a/server/services/image_edit.py b/server/services/image_edit.py new file mode 100644 index 0000000..3ec21fe --- /dev/null +++ b/server/services/image_edit.py @@ -0,0 +1,185 @@ +"""多模型图生图 / 图像编辑服务。 + +按模型路由到不同的 DashScope 调用形态: +- wanx2.1-imageedit → ImageSynthesis(function=description_edit) +- wan2.6-image / qwen-image-edit(-plus) → MultiModalConversation(多模态生成) + +职责单一:把请求翻译成 DashScope 调用并返回结果。当前不落盘、不存储图片; +后续接入七牛云时,可在返回结果 URL 之后增加「下载并转存」步骤,而不改 api 层与前端契约。 +""" +from __future__ import annotations + +import asyncio +import base64 +from http import HTTPStatus + +import httpx +from fastapi import HTTPException + +from config import get_settings +from schemas.image_edit import ImageEditRequest, ImageEditResponse, ImageEditResult + +# 服务端代理下载结果图时的体积上限,超出则跳过 base64(前端回退用 url) +_MAX_PROXY_BYTES = 20 * 1024 * 1024 + +# 走 ImageSynthesis 的模型(其余统一走 MultiModalConversation) +_WANX_IMAGEEDIT = "wanx2.1-imageedit" + + +def _download_to_data_url(url: str) -> str: + """把生成结果图下载并转成 data URL。 + + 目的:阿里云结果 URL 未开放 CORS,前端直接绘制到 canvas 会导致画布被污染、 + 无法 toDataURL 导出;由服务端代下载可彻底规避。失败时返回空串,前端回退用 url。 + """ + try: + with httpx.Client(timeout=60.0, follow_redirects=True) as client: + resp = client.get(url) + resp.raise_for_status() + content_type = (resp.headers.get("content-type") or "image/png").split(";")[0].strip() + if not content_type.startswith("image/"): + content_type = "image/png" + payload = resp.content + if len(payload) > _MAX_PROXY_BYTES: + return "" + b64 = base64.b64encode(payload).decode("ascii") + return f"data:{content_type};base64,{b64}" + except Exception: # noqa: BLE001 - 下载失败不影响主流程 + return "" + + +def _apply_base_url(settings) -> None: + """仅华北2(北京)业务空间需要配置 base_http_api_url。""" + import dashscope + + url = (settings.dashscope_base_http_api_url or "").strip() + if url: + dashscope.base_http_api_url = url + + +def _fail(rsp, label: str) -> None: + code = getattr(rsp, "code", None) + message = getattr(rsp, "message", "") + status = getattr(rsp, "status_code", "?") + raise HTTPException(status_code=502, detail=f"{label}失败(HTTP {status},code={code}):{message}") + + +def _to_results(urls: list[str]) -> list[ImageEditResult]: + return [ImageEditResult(url=u, image_base64=_download_to_data_url(u)) for u in urls] + + +def _call_wanx(req: ImageEditRequest, api_key: str) -> ImageEditResponse: + """wanx2.1-imageedit:ImageSynthesis 同步调用。""" + from dashscope import ImageSynthesis + + kwargs: dict = { + "api_key": api_key, + "model": "wanx2.1-imageedit", + "function": req.function, + "prompt": req.prompt, + "base_image_url": req.base_image, + "n": req.n, + } + if req.mask_image: + kwargs["mask_image_url"] = req.mask_image + if req.size: + kwargs["size"] = req.size + if req.seed is not None: + kwargs["seed"] = req.seed + if req.style: + kwargs["style"] = req.style + if req.prompt_extend: + kwargs["prompt_extend"] = True + if req.strength is not None: + # strength 走 SDK 的 **kwargs,最终落入请求体的 parameters + kwargs["strength"] = req.strength + + try: + rsp = ImageSynthesis.call(**kwargs) + except Exception as exc: # noqa: BLE001 - SDK 抛错类型不统一,统一转为 502 + raise HTTPException(status_code=502, detail=f"DashScope 调用异常:{exc}") from exc + + if rsp.status_code != HTTPStatus.OK: + _fail(rsp, "图生图") + + urls: list[str] = [] + for item in getattr(rsp.output, "results", None) or []: + url = item.get("url") if isinstance(item, dict) else getattr(item, "url", None) + if url: + urls.append(url) + + if not urls: + raise HTTPException(status_code=502, detail="模型未返回结果图片 URL") + + usage = getattr(rsp, "usage", None) + image_count = int(getattr(usage, "image_count", 0) or 0) if usage else len(urls) + return ImageEditResponse( + task_id=getattr(rsp.output, "task_id", ""), + results=_to_results(urls), + image_count=image_count or len(urls), + request_id=getattr(rsp, "request_id", ""), + ) + + +def _call_multimodal(req: ImageEditRequest, api_key: str) -> ImageEditResponse: + """wan2.6-image / qwen-image-edit(-plus[-快照]):MultiModalConversation。""" + from dashscope import MultiModalConversation + + messages = [{"role": "user", "content": [{"image": req.base_image}, {"text": req.prompt}]}] + kwargs: dict = { + "api_key": api_key, + "model": req.model, + "messages": messages, + "prompt_extend": req.prompt_extend, + } + if req.n: + kwargs["n"] = req.n + if req.size: + kwargs["size"] = req.size + + try: + rsp = MultiModalConversation.call(**kwargs) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=502, detail=f"DashScope 调用异常:{exc}") from exc + + if rsp.status_code != HTTPStatus.OK: + _fail(rsp, "图生图") + + urls: list[str] = [] + try: + content = rsp.output.choices[0].message.content + except (AttributeError, IndexError, KeyError, TypeError): + content = [] + if isinstance(content, list): + for item in content: + img = item.get("image") if isinstance(item, dict) else None + if img: + urls.append(img) + + if not urls: + raise HTTPException(status_code=502, detail="模型未返回结果图片") + + return ImageEditResponse( + task_id="", + results=_to_results(urls), + image_count=len(urls), + request_id=getattr(rsp, "request_id", ""), + ) + + +def _call_dashscope(req: ImageEditRequest, api_key: str) -> ImageEditResponse: + """同步调用 DashScope(在线程池中执行),按模型路由。""" + if req.model == _WANX_IMAGEEDIT: + return _call_wanx(req, api_key) + return _call_multimodal(req, api_key) + + +async def edit_image(req: ImageEditRequest) -> ImageEditResponse: + """入口:校验密钥后在独立线程中执行同步 SDK 调用。""" + settings = get_settings() + api_key = (settings.dashscope_api_key or "").strip() + if not api_key: + raise HTTPException(status_code=500, detail="未配置 DASHSCOPE_API_KEY,请写入 .env") + + _apply_base_url(settings) + return await asyncio.to_thread(_call_dashscope, req, api_key) diff --git a/start.command b/start.command index e8f86cb..65d47d6 100755 --- a/start.command +++ b/start.command @@ -19,12 +19,12 @@ if [[ ! -f .env ]]; then exit 1 fi -echo "启动中:http://127.0.0.1:8000/ozonSeller.html" +echo "启动中:http://127.0.0.1:8800/ozonSeller.html" echo "按 Ctrl+C 可停止服务" echo # --app-dir server 让 uvicorn 在 server/ 下找 main:app,工作目录仍是仓库根(.env 在此) -uvicorn main:app --app-dir server --reload --reload-include '*.yaml' --host 127.0.0.1 --port 8000 +uvicorn main:app --app-dir server --reload --reload-include '*.yaml' --host 127.0.0.1 --port 8800 echo echo "服务已停止。按回车关闭窗口…" diff --git a/studio/.env.example b/studio/.env.example new file mode 100644 index 0000000..d0f04cc --- /dev/null +++ b/studio/.env.example @@ -0,0 +1,4 @@ +# 前端环境变量(Vite 仅暴露 VITE_ 前缀变量) +# 开发环境默认走 vite 代理 /api -> http://127.0.0.1:8000,可留空 +VITE_API_BASE_URL=/api +VITE_APP_NAME=Ozon 发布工作台 diff --git a/studio/.gitignore b/studio/.gitignore new file mode 100644 index 0000000..7e4c82c --- /dev/null +++ b/studio/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.local +.env +.DS_Store diff --git a/studio/index.html b/studio/index.html new file mode 100644 index 0000000..675212d --- /dev/null +++ b/studio/index.html @@ -0,0 +1,12 @@ + + + + + + Ozon 发布工作台 + + +
+ + + diff --git a/studio/package.json b/studio/package.json new file mode 100644 index 0000000..d38ad93 --- /dev/null +++ b/studio/package.json @@ -0,0 +1,28 @@ +{ + "name": "studio", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^6.1.0", + "antd": "^6.1.3", + "axios": "^1.13.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router": "^7.11.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react-swc": "^4.2.2", + "typescript": "~5.9.3", + "vite": "^7.2.4" + }, + "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be" +} diff --git a/studio/pnpm-lock.yaml b/studio/pnpm-lock.yaml new file mode 100644 index 0000000..2a8011c --- /dev/null +++ b/studio/pnpm-lock.yaml @@ -0,0 +1,2043 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ant-design/icons': + specifier: ^6.1.0 + version: 6.3.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + antd: + specifier: ^6.1.3 + version: 6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + axios: + specifier: ^1.13.2 + version: 1.19.0 + react: + specifier: ^19.2.0 + version: 19.2.8 + react-dom: + specifier: ^19.2.0 + version: 19.2.8(react@19.2.8) + react-router: + specifier: ^7.11.0 + version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + '@types/react': + specifier: ^19.2.5 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.4(@types/react@19.2.18) + '@vitejs/plugin-react-swc': + specifier: ^4.2.2 + version: 4.3.3(vite@7.3.6(@types/node@24.13.3)) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.2.4 + version: 7.3.6(@types/node@24.13.3) + +packages: + + '@ant-design/colors@8.0.1': + resolution: {integrity: sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==} + + '@ant-design/cssinjs-utils@2.1.2': + resolution: {integrity: sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + '@ant-design/cssinjs@2.1.2': + resolution: {integrity: sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/fast-color@3.0.1': + resolution: {integrity: sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==} + engines: {node: '>=8.x'} + + '@ant-design/icons-svg@4.5.0': + resolution: {integrity: sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==} + + '@ant-design/icons@6.3.2': + resolution: {integrity: sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/react-slick@2.0.0': + resolution: {integrity: sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==} + peerDependencies: + react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@8.0.0': + resolution: {integrity: sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==} + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@emotion/unitless@0.7.5': + resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rc-component/async-validator@6.0.0': + resolution: {integrity: sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==} + engines: {node: '>=14.x'} + + '@rc-component/cascader@1.22.0': + resolution: {integrity: sha512-SffrA57aS9oub3VuI7ajPhJTPtaNxngSvtRhD40Rd8dwJ5vfWPSrVanWgeepdWFGBt7EHftIK5RUU0u3rCTwWw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/checkbox@2.0.0': + resolution: {integrity: sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/collapse@1.2.0': + resolution: {integrity: sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/color-picker@3.1.1': + resolution: {integrity: sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/context@2.0.2': + resolution: {integrity: sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/dialog@1.10.0': + resolution: {integrity: sha512-eDukNlz9vNszAGv7i3zKXdxEd3wgVmNxuJijYt8zvTh17QwTu8KK/bdURRd/lU4qaMzhO1HKKmMrwOnkaw0BvQ==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/drawer@1.4.2': + resolution: {integrity: sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/dropdown@1.0.3': + resolution: {integrity: sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g==} + peerDependencies: + react: '>=16.11.0' + react-dom: '>=16.11.0' + + '@rc-component/form@1.8.6': + resolution: {integrity: sha512-1EXVsSKPZC6kGrIqxVck7kAWM6T65b6et/n5wIcyiaIa41VrDshD0nVLHoBDEIZr2ATQsOP6icb4XQkNFMLBAw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/image@1.10.0': + resolution: {integrity: sha512-BjeZCRQ+hw+4WAhvrw8rJvy5fckA2xpf/X2XQEOABUHvLTNB9inB98X3Mp54jYQ7g10DfWERQWHXeC4ylxp1Uw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/input-number@1.6.2': + resolution: {integrity: sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/input@1.3.1': + resolution: {integrity: sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@rc-component/listy@1.2.3': + resolution: {integrity: sha512-IXiMjV5s0rczLBlfh7G5nB4M3365mrEeedjwKtf5I+Ns3PqRUsebR2h5u8CeFarsVfLUPC2I5p0h09TNoOWyvQ==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/mentions@1.11.0': + resolution: {integrity: sha512-IC2qXuEBMFHxPIXEFfYWj6Sr7UiDZnOqJHCYQBbwPzopBJOPZIR6mV9U4QH1bYQRlKYlYnIsajWDMgVGgWQyWQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/menu@1.4.1': + resolution: {integrity: sha512-3GsVRoQ4cnF/AoIQ4P+Z1haBfgfBPQfLT1RJY3Nu4DzOnheTslfCiGSPj7bv/cLj5sW5pHqN25dDXGP3JELAlQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/mini-decimal@1.1.4': + resolution: {integrity: sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==} + engines: {node: '>=8.x'} + + '@rc-component/motion@1.3.3': + resolution: {integrity: sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/mutate-observer@2.0.1': + resolution: {integrity: sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/notification@2.0.7': + resolution: {integrity: sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/overflow@1.0.1': + resolution: {integrity: sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/pagination@1.4.0': + resolution: {integrity: sha512-CW1g7P9V8u+e8JQdUsl2RWg+GCsoee0mtJjZUCCxn/vb3jzOwDKm6hAdwddHCVBfWJ58eGUBZz3IvnU8rRktjw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/picker@1.12.0': + resolution: {integrity: sha512-0FZGgDiDZFMm2hcfGv4jyjm7yWIj2MiwfeTzLL0vWkO+8cCSKdiM9XhWpkn1aAsoI2EwUy48Oz2cGfvL8ZKrfw==} + engines: {node: '>=12.x'} + peerDependencies: + date-fns: '>= 2.x' + dayjs: '>= 1.x' + luxon: '>= 3.x' + moment: '>= 2.x' + react: '>=16.9.0' + react-dom: '>=16.9.0' + peerDependenciesMeta: + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + + '@rc-component/portal@2.2.1': + resolution: {integrity: sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==} + engines: {node: '>=12.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/progress@1.0.2': + resolution: {integrity: sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/qrcode@2.0.0': + resolution: {integrity: sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/rate@1.0.1': + resolution: {integrity: sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/resize-observer@1.1.2': + resolution: {integrity: sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/segmented@1.3.0': + resolution: {integrity: sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@rc-component/select@1.10.1': + resolution: {integrity: sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '*' + react-dom: '*' + + '@rc-component/slider@1.1.1': + resolution: {integrity: sha512-LSzgWGYDgeCDgR4r1XlU29gbYws6HpLnvJd/uMhLeW/vQgxldeR+Wb4uzHDCHiYEbr1bnEHWdjkPxjJRHxuiig==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/steps@1.2.2': + resolution: {integrity: sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/switch@1.0.3': + resolution: {integrity: sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/table@1.11.1': + resolution: {integrity: sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/tabs@1.12.0': + resolution: {integrity: sha512-XL7Kqy5fnUE2WTlO1/fCGrrfNlGFebdr7JseGkEIjzcVMAtIFQJ8sqCSOmxcXstjU6fonD/4rnhZHxj7sDTajQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/tooltip@1.5.0': + resolution: {integrity: sha512-agQ/+mBqrEQfTX4D3KhQ7j+ZbX4/VHjoJ7Noa2wIdZ1/FbQTOd7Sn92rp+jtCoqAVTLUgSOydePIgZ204gi2EQ==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/tour@2.4.0': + resolution: {integrity: sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/tree-select@1.16.1': + resolution: {integrity: sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg==} + peerDependencies: + react: '*' + react-dom: '*' + + '@rc-component/tree@1.4.0': + resolution: {integrity: sha512-dGsJGDJQedA0BqqVgj3F8BvHXTSZijyhTXdbAdkcx8lynzZkty/CV3Z3LOm/fxz+BCfl3dfGiAQpb7Q5XNvl0Q==} + engines: {node: '>=10.x'} + peerDependencies: + react: '*' + react-dom: '*' + + '@rc-component/trigger@3.10.1': + resolution: {integrity: sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/upload@1.1.1': + resolution: {integrity: sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/util@1.12.0': + resolution: {integrity: sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rc-component/virtual-list@1.5.1': + resolution: {integrity: sha512-boqHxdtyWC88u8quYgEO49bcBy5fzRiOcnBge+N4nLzs2k8hUQ/yw7JE9dM6yCBE4jSm5YSHVCVMS+suBuJGKA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@swc/core-darwin-arm64@1.15.47': + resolution: {integrity: sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.47': + resolution: {integrity: sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.47': + resolution: {integrity: sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.47': + resolution: {integrity: sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.47': + resolution: {integrity: sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.47': + resolution: {integrity: sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.47': + resolution: {integrity: sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.47': + resolution: {integrity: sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.47': + resolution: {integrity: sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.47': + resolution: {integrity: sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.47': + resolution: {integrity: sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.47': + resolution: {integrity: sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.47': + resolution: {integrity: sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.28': + resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + + '@vitejs/plugin-react-swc@4.3.3': + resolution: {integrity: sha512-bti8ZAcvz4Lh6/e4Uk2k3aa1TiUXbbMsahuqOHvd3MveFTkKDZOA6wQVkpj7J/+tepX/wGfe+lsGh/t24HTXMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 || ^8 + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + antd@6.6.0: + resolution: {integrity: sha512-UDwWIbpmrCHB9ZQ+bPh4vQfB6DTI2ulIyoQ0Tc9xxalFblttiNGHl3ySBD9SyV/8+gUjFzfSx1+iU1Fog2i46w==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + compute-scroll-into-view@3.1.1: + resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + is-mobile@5.0.0: + resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==} + + json2mq@0.2.0: + resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-is@19.2.8: + resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} + + react-router@7.18.2: + resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-convert@0.2.1: + resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + throttle-debounce@5.0.2: + resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} + engines: {node: '>=12.22'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@ant-design/colors@8.0.1': + dependencies: + '@ant-design/fast-color': 3.0.1 + + '@ant-design/cssinjs-utils@2.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@babel/runtime': 7.29.7 + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@ant-design/cssinjs@2.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@emotion/hash': 0.8.0 + '@emotion/unitless': 0.7.5 + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + csstype: 3.2.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + stylis: 4.4.0 + + '@ant-design/fast-color@3.0.1': {} + + '@ant-design/icons-svg@4.5.0': {} + + '@ant-design/icons@6.3.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@ant-design/colors': 8.0.1 + '@ant-design/icons-svg': 4.5.0 + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@ant-design/react-slick@2.0.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + clsx: 2.1.1 + json2mq: 0.2.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + throttle-debounce: 5.0.2 + + '@babel/runtime@7.29.7': {} + + '@babel/runtime@8.0.0': {} + + '@emotion/hash@0.8.0': {} + + '@emotion/unitless@0.7.5': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@rc-component/async-validator@6.0.0': + dependencies: + '@babel/runtime': 7.29.7 + + '@rc-component/cascader@1.22.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/select': 1.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/tree': 1.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/checkbox@2.0.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/collapse@1.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/color-picker@3.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@ant-design/fast-color': 3.0.1 + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/context@2.0.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/dialog@1.10.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/portal': 2.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/drawer@1.4.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/portal': 2.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/dropdown@1.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/form@1.8.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/async-validator': 6.0.0 + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/image@1.10.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/portal': 2.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/input-number@1.6.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/mini-decimal': 1.1.4 + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/input@1.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/listy@1.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/portal': 2.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/virtual-list': 1.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/mentions@1.11.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/input': 1.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/menu': 1.4.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/menu@1.4.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/overflow': 1.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/mini-decimal@1.1.4': + dependencies: + '@babel/runtime': 7.29.7 + + '@rc-component/motion@1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/mutate-observer@2.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/notification@2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/overflow@1.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/pagination@1.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/picker@1.12.0(dayjs@1.11.21)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/overflow': 1.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + dayjs: 1.11.21 + + '@rc-component/portal@2.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/progress@1.0.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/qrcode@2.0.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/rate@1.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/resize-observer@1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/segmented@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/select@1.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/overflow': 1.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/virtual-list': 1.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/slider@1.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/steps@1.2.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/switch@1.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/table@1.11.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/context': 2.0.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/virtual-list': 1.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/tabs@1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/dropdown': 1.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/menu': 1.4.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/tooltip@1.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/tour@2.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/portal': 2.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/tree-select@1.16.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/select': 1.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/tree': 1.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/tree@1.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/virtual-list': 1.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/trigger@3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/portal': 2.2.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/upload@1.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rc-component/util@1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + is-mobile: 5.0.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-is: 19.2.8 + + '@rc-component/virtual-list@1.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 8.0.0 + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@swc/core-darwin-arm64@1.15.47': + optional: true + + '@swc/core-darwin-x64@1.15.47': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.47': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.47': + optional: true + + '@swc/core-linux-arm64-musl@1.15.47': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.47': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.47': + optional: true + + '@swc/core-linux-x64-gnu@1.15.47': + optional: true + + '@swc/core-linux-x64-musl@1.15.47': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.47': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.47': + optional: true + + '@swc/core-win32-x64-msvc@1.15.47': + optional: true + + '@swc/core@1.15.47': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.28 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.47 + '@swc/core-darwin-x64': 1.15.47 + '@swc/core-linux-arm-gnueabihf': 1.15.47 + '@swc/core-linux-arm64-gnu': 1.15.47 + '@swc/core-linux-arm64-musl': 1.15.47 + '@swc/core-linux-ppc64-gnu': 1.15.47 + '@swc/core-linux-s390x-gnu': 1.15.47 + '@swc/core-linux-x64-gnu': 1.15.47 + '@swc/core-linux-x64-musl': 1.15.47 + '@swc/core-win32-arm64-msvc': 1.15.47 + '@swc/core-win32-ia32-msvc': 1.15.47 + '@swc/core-win32-x64-msvc': 1.15.47 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.28': + dependencies: + '@swc/counter': 0.1.3 + + '@types/estree@1.0.9': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.2.4(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react-swc@4.3.3(vite@7.3.6(@types/node@24.13.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + '@swc/core': 1.15.47 + vite: 7.3.6(@types/node@24.13.3) + transitivePeerDependencies: + - '@swc/helpers' + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + antd@6.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@ant-design/colors': 8.0.1 + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@ant-design/cssinjs-utils': 2.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@ant-design/fast-color': 3.0.1 + '@ant-design/icons': 6.3.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@ant-design/react-slick': 2.0.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@babel/runtime': 7.29.7 + '@rc-component/cascader': 1.22.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/checkbox': 2.0.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/collapse': 1.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/color-picker': 3.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/dialog': 1.10.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/drawer': 1.4.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/dropdown': 1.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/form': 1.8.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/image': 1.10.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/input': 1.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/input-number': 1.6.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/listy': 1.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/mentions': 1.11.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/menu': 1.4.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/motion': 1.3.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/mutate-observer': 2.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/notification': 2.0.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/pagination': 1.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/picker': 1.12.0(dayjs@1.11.21)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/progress': 1.0.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/qrcode': 2.0.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/rate': 1.0.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/segmented': 1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/select': 1.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/slider': 1.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/steps': 1.2.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/switch': 1.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/table': 1.11.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/tabs': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/tooltip': 1.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/tour': 2.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/tree': 1.4.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/tree-select': 1.16.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/trigger': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/upload': 1.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@rc-component/util': 1.12.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + clsx: 2.1.1 + dayjs: 1.11.21 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + scroll-into-view-if-needed: 3.1.0 + throttle-debounce: 5.0.2 + transitivePeerDependencies: + - date-fns + - luxon + - moment + + asynckit@0.4.0: {} + + axios@1.19.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + clsx@2.1.1: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + compute-scroll-into-view@3.1.1: {} + + cookie@1.1.1: {} + + csstype@3.2.3: {} + + dayjs@1.11.21: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + follow-redirects@1.16.0: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + is-mobile@5.0.0: {} + + json2mq@0.2.0: + dependencies: + string-convert: 0.2.1 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@2.1.0: {} + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@19.2.8: {} + + react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie: 1.1.1 + react: 19.2.8 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + + react@19.2.8: {} + + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + scroll-into-view-if-needed@3.1.0: + dependencies: + compute-scroll-into-view: 3.1.1 + + set-cookie-parser@2.7.2: {} + + source-map-js@1.2.1: {} + + string-convert@0.2.1: {} + + stylis@4.4.0: {} + + throttle-debounce@5.0.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + vite@7.3.6(@types/node@24.13.3): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.26 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 diff --git a/studio/public/imgs/panda_shop_200x200_opacity30.png b/studio/public/imgs/panda_shop_200x200_opacity30.png new file mode 100644 index 0000000000000000000000000000000000000000..ab0063cc8cbe3522597270971a6e5feec1b79904 GIT binary patch literal 84296 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 s+fe@U7e8xG_HhzvS4qi)){C(@wR7b^so5ENB_$ + + + + + ); +} + +export default App; diff --git a/studio/src/config/env.ts b/studio/src/config/env.ts new file mode 100644 index 0000000..ac6573e --- /dev/null +++ b/studio/src/config/env.ts @@ -0,0 +1,20 @@ +/** + * 环境变量配置(Vite 仅暴露 VITE_ 前缀变量) + */ + +interface EnvConfig { + apiBaseUrl: string; + appName: string; + debug: boolean; +} + +const getEnv = (key: string, defaultValue: string = ''): string => { + return import.meta.env[key] || defaultValue; +}; + +export const envConfig: EnvConfig = { + // 开发环境默认走 vite 代理 /api -> http://127.0.0.1:8800 + apiBaseUrl: getEnv('VITE_API_BASE_URL', '/api'), + appName: getEnv('VITE_APP_NAME', 'Ozon 发布工作台'), + debug: import.meta.env.DEV, +}; diff --git a/studio/src/index.css b/studio/src/index.css new file mode 100644 index 0000000..ffd8409 --- /dev/null +++ b/studio/src/index.css @@ -0,0 +1,35 @@ +:root { + --primary-color: #8b5cf6; + --primary-light: #a78bfa; + --primary-dark: #7c3aed; + /* 侧边菜单背景(纯色,中间调;想再调直接改这里) */ + --sider-bg: #6d28d9; + --text-primary: #1f2937; + --text-secondary: #6b7280; + --bg-color: #f9fafb; + --card-bg: #ffffff; + --border-color: #e5e7eb; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', + 'Microsoft YaHei', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: var(--bg-color); + color: var(--text-primary); + line-height: 1.6; +} + +#root { + min-height: 100vh; +} diff --git a/studio/src/layouts/MainLayout.css b/studio/src/layouts/MainLayout.css new file mode 100644 index 0000000..c07c96a --- /dev/null +++ b/studio/src/layouts/MainLayout.css @@ -0,0 +1,115 @@ +.main-layout { + min-height: 100vh; +} + +.main-sider { + background: var(--sider-bg) !important; + display: flex; + flex-direction: column; +} + +.sidebar-header { + padding: 24px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + text-align: center; +} + +.logo { + font-size: 22px; + font-weight: 700; + color: white; + margin-bottom: 4px; +} + +.logo-subtitle { + font-size: 12px; + color: rgba(255, 255, 255, 0.8); +} + +.main-header { + background: white !important; + padding: 0 24px !important; + display: flex; + align-items: center; + justify-content: space-between; + box-shadow: + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); + position: sticky; + top: 0; + z-index: 100; + height: 64px; + line-height: 1; +} + +.header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.collapse-trigger, +.mobile-menu-trigger { + font-size: 18px; + cursor: pointer; + color: #1f2937; + border-radius: 4px; + transition: background 0.2s; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + flex-shrink: 0; +} + +.collapse-trigger:hover, +.mobile-menu-trigger:hover { + background: #f9fafb; +} + +.page-title-group { + display: flex; + flex-direction: column; + justify-content: center; + line-height: 1.4; +} + +.page-title { + font-size: 20px; + font-weight: 600; + color: #1f2937; + line-height: 1.2; + margin-bottom: 4px; +} + +.page-subtitle { + font-size: 14px; + color: #6b7280; + font-style: italic; + line-height: 1.2; +} + +.main-content { + padding: 24px; + background: #f3f4f6; + min-height: calc(100vh - 64px); +} + +@media (max-width: 768px) { + .main-content { + padding: 16px; + } + + .main-header { + padding: 0 16px !important; + } + + .page-title { + font-size: 18px; + } + + .page-subtitle { + font-size: 12px; + } +} diff --git a/studio/src/layouts/MainLayout.tsx b/studio/src/layouts/MainLayout.tsx new file mode 100644 index 0000000..e3e9cbb --- /dev/null +++ b/studio/src/layouts/MainLayout.tsx @@ -0,0 +1,102 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Outlet, useLocation } from 'react-router'; +import { Drawer, Layout } from 'antd'; +import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; +import SidebarMenu from './SidebarMenu'; +import { getPageInfo } from './menuConfig'; +import './MainLayout.css'; + +const { Header, Sider, Content } = Layout; + +const MainLayout = () => { + const [collapsed, setCollapsed] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const location = useLocation(); + + const pageInfo = useMemo(() => getPageInfo(location.pathname), [location.pathname]); + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); + if (window.innerWidth >= 768) setMobileMenuOpen(false); + }; + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + return ( + + {!isMobile && ( + +
+
{collapsed ? 'O' : 'Ozon Kit'}
+ {!collapsed &&
发布工作台
} +
+ +
+ )} + + {isMobile && ( + setMobileMenuOpen(false)} + open={mobileMenuOpen} + styles={{ body: { padding: 0, background: 'var(--sider-bg)' } }} + width={240} + > + setMobileMenuOpen(false)} /> + + )} + + +
+
+ {isMobile && ( + setMobileMenuOpen(true)} + /> + )} + {!isMobile && ( +
setCollapsed(!collapsed)}> + {collapsed ? : } +
+ )} +
+
{pageInfo.title}
+
{pageInfo.subtitle}
+
+
+
+ + + + +
+
+ ); +}; + +export default MainLayout; diff --git a/studio/src/layouts/SidebarMenu.css b/studio/src/layouts/SidebarMenu.css new file mode 100644 index 0000000..dee6f15 --- /dev/null +++ b/studio/src/layouts/SidebarMenu.css @@ -0,0 +1,82 @@ +.sidebar-menu-container { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.sidebar-menu { + background: transparent !important; + border: none; + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +.sidebar-menu .ant-menu-item { + margin: 0 !important; + padding: 12px 20px !important; + height: auto !important; + line-height: 1.5 !important; + transition: all 0.3s !important; + font-size: 15px !important; + font-weight: 600 !important; +} + +.sidebar-menu .ant-menu-item .ant-menu-title-content { + font-size: 15px !important; + font-weight: 600 !important; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: inline-block; +} + +.sidebar-menu .ant-menu-item:hover { + background: rgba(255, 255, 255, 0.1) !important; +} + +.sidebar-menu .ant-menu-item-selected { + background: rgba(255, 255, 255, 0.2) !important; + border-left: 3px solid white; +} + +.sidebar-menu .ant-menu-item-selected .ant-menu-title-content { + transform: translateX(8px); +} + +.sidebar-menu .ant-menu-item-selected:hover { + background: rgba(255, 255, 255, 0.25) !important; +} + +.sidebar-menu .ant-menu-item-selected:hover .ant-menu-title-content { + transform: translateX(8px); +} + +.sidebar-menu .ant-menu-item-group-title { + padding: 12px 20px 8px !important; + font-size: 12px !important; + color: rgba(255, 255, 255, 0.7) !important; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 500; +} + +.sidebar-menu .ant-menu-item-icon { + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 18px !important; + display: inline-flex; + align-items: center; +} + +.sidebar-menu .ant-menu-item:hover .ant-menu-item-icon { + transform: scale(1.05); +} + +.sidebar-menu .ant-menu-item-selected .ant-menu-item-icon { + transform: translateX(8px); +} + +@media (max-width: 768px) { + .sidebar-menu .ant-menu-item { + padding: 14px 20px !important; + } +} diff --git a/studio/src/layouts/SidebarMenu.tsx b/studio/src/layouts/SidebarMenu.tsx new file mode 100644 index 0000000..74953c1 --- /dev/null +++ b/studio/src/layouts/SidebarMenu.tsx @@ -0,0 +1,61 @@ +import { useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router'; +import { Menu } from 'antd'; +import type { MenuProps } from 'antd'; +import { routeMenuConfig } from './menuConfig'; +import './SidebarMenu.css'; + +interface SidebarMenuProps { + collapsed: boolean; + onMenuClick?: () => void; +} + +const SidebarMenu = ({ collapsed, onMenuClick }: SidebarMenuProps) => { + const location = useLocation(); + const navigate = useNavigate(); + + const menuItems: MenuProps['items'] = useMemo(() => { + const children = routeMenuConfig.map((config) => ({ + key: config.key, + icon: config.icon, + label: config.label, + })); + return [ + { + type: 'group', + label: collapsed ? '' : '主要功能', + className: 'menu-section-title', + children, + }, + ]; + }, [collapsed]); + + const selectedKeys = useMemo(() => { + const path = location.pathname; + if (path === '/' || path === '') { + return [routeMenuConfig[0].key]; + } + return [path]; + }, [location.pathname]); + + const handleMenuClick: MenuProps['onClick'] = ({ key }) => { + navigate(key); + if (onMenuClick) onMenuClick(); + }; + + return ( +
+ +
+ ); +}; + +export default SidebarMenu; diff --git a/studio/src/layouts/menuConfig.tsx b/studio/src/layouts/menuConfig.tsx new file mode 100644 index 0000000..ffaf2f6 --- /dev/null +++ b/studio/src/layouts/menuConfig.tsx @@ -0,0 +1,39 @@ +import { PictureOutlined } from '@ant-design/icons'; +import type { ReactNode } from 'react'; + +export interface RouteMenuConfig { + /** 路由路径 */ + path: string; + /** 菜单键值(与 path 相同) */ + key: string; + /** 菜单图标 */ + icon: ReactNode; + /** 菜单标签 */ + label: string; + /** 页面标题 */ + title: string; + /** 页面副标题 */ + subtitle: string; +} + +/** 菜单配置列表(当前仅「AI 图生图」一页) */ +export const routeMenuConfig: RouteMenuConfig[] = [ + { + path: '/ai-image', + key: '/ai-image', + icon: , + label: 'AI 图生图', + title: 'AI 图生图', + subtitle: '上传图片、加水印、用万相模型进行图生图编辑', + }, +]; + +/** 根据路径获取页面标题信息 */ +export const getPageInfo = (path: string): { title: string; subtitle: string } => { + if (path === '/' || path === '') { + const first = routeMenuConfig[0]; + return { title: first.title, subtitle: first.subtitle }; + } + const config = routeMenuConfig.find((item) => item.path === path); + return config ? { title: config.title, subtitle: config.subtitle } : getPageInfo('/'); +}; diff --git a/studio/src/main.tsx b/studio/src/main.tsx new file mode 100644 index 0000000..00f5203 --- /dev/null +++ b/studio/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/studio/src/pages/ai-image/AiImagePage.css b/studio/src/pages/ai-image/AiImagePage.css new file mode 100644 index 0000000..2ab103d --- /dev/null +++ b/studio/src/pages/ai-image/AiImagePage.css @@ -0,0 +1,538 @@ +.ai-image-page { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 1200px; +} + +.panel { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 20px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04); +} + +.panel-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16px; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; +} + +.panel-header .panel-title { + margin-bottom: 0; +} + +/* 水印设置 */ +.settings-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 32px; +} + +.settings-item { + display: flex; + align-items: center; + gap: 10px; +} + +.settings-label { + font-size: 14px; + color: var(--text-secondary); + white-space: nowrap; +} + +.settings-hint { + font-size: 12px; + color: var(--text-secondary); +} + +.wm-preview-img { + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid var(--border-color); + object-fit: cover; +} + +.settings-opacity { + flex: 1; + min-width: 240px; + max-width: 420px; +} + +/* 上传区 */ +.panel .ant-upload-wrapper .ant-upload-drag { + background: #fafafa; +} + +/* 预览网格 */ +.image-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; +} + +.image-card { + display: flex; + flex-direction: column; + gap: 8px; + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 10px; + background: #fff; +} + +.watermark-canvas { + display: block; + width: 100%; + height: auto; + border-radius: 6px; + background: + repeating-conic-gradient(#f3f4f6 0% 25%, #fff 0% 50%) 50% / 16px 16px; +} + +.watermark-canvas-draggable { + cursor: grab; +} + +.watermark-canvas-draggable:active { + cursor: grabbing; +} + +.image-card-name { + font-size: 12px; + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.image-card-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.drag-hint { + margin-top: 12px; + font-size: 12px; + color: var(--text-secondary); +} + +/* ===== 图片编辑工作台(AI + 精确标注) ===== */ +.workbench-root { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} + +.workbench { + display: flex; + gap: 12px; + flex: 1; + min-height: 0; +} + +.wb-left { + width: 112px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; +} + +.wb-thumb { + cursor: pointer; + border: 2px solid transparent; + border-radius: 8px; + padding: 4px; + background: #fff; + transition: border-color 0.2s; +} + +.wb-thumb img { + width: 100%; + height: 78px; + object-fit: contain; + border-radius: 4px; + background: + repeating-conic-gradient(#f3f4f6 0% 25%, #fff 0% 50%) 50% / 16px 16px; +} + +.wb-thumb span { + display: block; + font-size: 12px; + text-align: center; + color: var(--text-secondary); +} + +.wb-thumb.active { + border-color: var(--primary-color); +} + +.wb-thumb.active span { + color: var(--primary-color); +} + +.wb-thumb-empty { + font-size: 12px; + color: var(--text-secondary); + text-align: center; + padding: 8px; +} + +.wb-center { + /* 中间图片区域:宽度更窄(接近电商商品图竖版比例),水平居中、顶部与标注编辑区对齐 */ + width: min(680px, 72%); + height: 100%; + margin: 0 auto; + align-self: flex-start; + min-width: 0; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + background: #f3f4f6; + border: 1px solid var(--border-color); + border-radius: 10px; +} + +.wb-loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.annotation-canvas-wrap { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 12px; +} + +.annotation-canvas { + max-width: 100%; + max-height: 100%; + cursor: default; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +/* 标注编辑区(右侧面板) */ +.wb-right { + width: 340px; + flex-shrink: 0; + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; + overflow-x: hidden; + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 8px 12px 12px; + background: #fff; +} + +.wb-tabs { + flex-shrink: 0; +} + +.wb-add-row { + display: flex; + gap: 8px; + margin-bottom: 10px; + flex-shrink: 0; +} + +.wb-add-row .ant-btn { + flex: 1; +} + +.wb-preset-row { + margin-bottom: 10px; + flex-shrink: 0; +} + +.wb-preset-select { + width: 100%; +} + +.wb-save-row { + margin-top: 10px; + flex-shrink: 0; +} + +.wb-props { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +/* 属性行内控件允许收缩,避免横向滚动条 */ +.prop-row > * { + min-width: 0; +} + +.wb-edit-actions { + display: flex; + gap: 8px; + padding-top: 10px; + margin-top: 10px; + border-top: 1px solid var(--border-color); + flex-shrink: 0; +} + +.wb-edit-actions .ant-btn { + flex: 1; +} + +.props-empty { + padding-top: 24px; +} + +.props-hint { + font-size: 12px; + color: var(--text-secondary); + text-align: center; +} + +.props-panel { + display: flex; + flex-direction: column; + gap: 12px; +} + +.prop-row { + display: flex; + align-items: center; + gap: 8px; +} + +.prop-row .ant-input, +.prop-row .ant-input-textarea, +.prop-row textarea { + flex: 1; +} + +.prop-label { + width: 62px; + font-size: 13px; + color: var(--text-secondary); + flex-shrink: 0; +} + +.wb-bottom { + margin-top: 12px; + display: flex; + flex-direction: column; + gap: 10px; + flex-shrink: 0; +} + +.wb-ai { + display: flex; + gap: 12px; + height: 96px; + align-items: stretch; +} + +.wb-ai-input { + flex: 1; + min-width: 0; + height: 100%; +} + +/* 右侧:模型下拉 + AI 生成,宽度与标注编辑区(340px)一致 */ +.wb-ai-side { + width: 340px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.wb-model-select { + width: 100%; +} + +.wb-ai-side .ant-btn { + flex: 1; + min-height: 44px; +} + +/* ===== 旧的 AI 弹窗样式(保留兼容) ===== */ +.edit-modal { + display: flex; + flex-direction: column; + gap: 16px; +} + +.edit-modal-compare { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.compare-pane { + border: 1px solid var(--border-color); + border-radius: 10px; + overflow: hidden; +} + +.pane-title { + padding: 8px 12px; + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + background: #f9fafb; + border-bottom: 1px solid var(--border-color); +} + +.pane-body { + min-height: 320px; + display: flex; + align-items: center; + justify-content: center; + background: + repeating-conic-gradient(#f3f4f6 0% 25%, #fff 0% 50%) 50% / 16px 16px; + padding: 12px; +} + +.pane-img { + max-width: 100%; + max-height: 420px; + object-fit: contain; +} + +.pane-loading { + display: flex; + align-items: center; + justify-content: center; + height: 320px; +} + +/* 对话框 */ +.edit-modal-chat { + display: flex; + flex-direction: column; + gap: 12px; +} + +.chat-messages { + height: 220px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 12px; + background: #fafafa; + display: flex; + flex-direction: column; + gap: 10px; +} + +.chat-placeholder { + margin: auto; + color: var(--text-secondary); + font-size: 13px; + text-align: center; +} + +.chat-message { + display: flex; +} + +.chat-user { + justify-content: flex-end; +} + +.chat-assistant { + justify-content: flex-start; +} + +.chat-bubble { + max-width: 80%; + padding: 8px 12px; + border-radius: 10px; + font-size: 14px; + line-height: 1.5; + word-break: break-word; +} + +.chat-user .chat-bubble { + background: var(--primary-color); + color: #fff; + border-bottom-right-radius: 2px; +} + +.chat-assistant .chat-bubble { + background: #fff; + border: 1px solid var(--border-color); + color: var(--text-primary); + border-bottom-left-radius: 2px; +} + +.chat-img { + display: block; + margin-top: 8px; + max-width: 100%; + max-height: 160px; + border-radius: 6px; +} + +.chat-options { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; +} + +.chat-strength { + display: flex; + align-items: center; + gap: 8px; +} + +.chat-strength-label { + font-size: 13px; + color: var(--text-secondary); + white-space: nowrap; +} + +.chat-strength-value { + font-size: 13px; + color: var(--text-primary); + min-width: 2.5em; +} + +.chat-strength-hint { + font-size: 12px; + color: var(--text-secondary); +} + +.chat-input-row { + display: flex; + gap: 12px; + align-items: flex-end; +} + +.chat-input-row .ant-input { + flex: 1; +} diff --git a/studio/src/pages/ai-image/AiImagePage.tsx b/studio/src/pages/ai-image/AiImagePage.tsx new file mode 100644 index 0000000..08dcc82 --- /dev/null +++ b/studio/src/pages/ai-image/AiImagePage.tsx @@ -0,0 +1,266 @@ +import { useEffect, useRef, useState } from 'react'; +import { App, Button, Empty, Input, Radio, Slider, Space, Upload } from 'antd'; +import { + DeleteOutlined, + DownloadOutlined, + ExperimentOutlined, + InboxOutlined, +} from '@ant-design/icons'; +import type { ImageItem, WatermarkState, WatermarkType } from '@/types/image'; +import { canvasToDataUrl, downloadCanvas, fileToImage } from '@/utils/image'; +import { defaultPos, renderFullRes } from '@/utils/watermark'; +import WatermarkCanvas from './components/WatermarkCanvas'; +import ImageEditModal from './components/ImageEditModal'; +import './AiImagePage.css'; + +const DEFAULT_WM_TEXT = 'Panda Store'; +const DEFAULT_OPACITY = 30; +const WATERMARK_IMG_SRC = '/imgs/watermark.jpg'; + +type WatermarkKind = Exclude; + +const AiImagePage = () => { + const { message } = App.useApp(); + + const [items, setItems] = useState([]); + const seqRef = useRef(0); + + // 全局水印设置 + const [wmType, setWmType] = useState('image'); + const [wmText, setWmText] = useState(DEFAULT_WM_TEXT); + const [wmOpacity, setWmOpacity] = useState(DEFAULT_OPACITY); + const [watermarkImg, setWatermarkImg] = useState(null); + + // 弹窗 + const [modalOpen, setModalOpen] = useState(false); + const [editing, setEditing] = useState<{ dataUrl: string; fileName: string } | null>(null); + + useEffect(() => { + const img = new Image(); + img.onload = () => setWatermarkImg(img); + img.onerror = () => console.error('水印图片加载失败:', WATERMARK_IMG_SRC); + img.src = WATERMARK_IMG_SRC; + }, []); + + function buildState(imgWidth: number, imgHeight: number): WatermarkState { + const state: WatermarkState = { + type: wmType, + text: wmText, + opacity: wmOpacity, + pos: { x: 0, y: 0 }, + }; + state.pos = defaultPos(imgWidth, imgHeight, state); + return state; + } + + function validateWatermark(): boolean { + if (wmType === 'text' && !wmText.trim()) { + message.warning('请先填写水印文字'); + return false; + } + if (wmType === 'image' && !watermarkImg) { + message.warning('水印图片加载中,请稍后'); + return false; + } + return true; + } + + const addFile = async (file: File) => { + try { + const { image, dataUrl } = await fileToImage(file); + const item: ImageItem = { + id: seqRef.current++, + fileName: file.name, + image, + dataUrl, + state: buildState(image.width, image.height), + }; + setItems((prev) => [...prev, item]); + } catch { + message.error(`图片读取失败:${file.name}`); + } + }; + + const handleBeforeUpload = (file: File) => { + if (file.type.startsWith('image/')) { + void addFile(file); + } + return false; + }; + + const updateItemState = (id: number, patch: Partial) => { + setItems((prev) => + prev.map((it) => (it.id === id ? { ...it, state: { ...it.state, ...patch } } : it)), + ); + }; + + const applyToAll = () => { + if (items.length === 0) { + message.warning('请先上传图片'); + return; + } + if (!validateWatermark()) return; + setItems((prev) => + prev.map((it) => ({ ...it, state: buildState(it.image.width, it.image.height) })), + ); + }; + + const toggleWatermark = (item: ImageItem) => { + if (item.state.type) { + updateItemState(item.id, { type: null }); + return; + } + if (!validateWatermark()) return; + const next = buildState(item.image.width, item.image.height); + setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, state: next } : it))); + }; + + const deleteItem = (id: number) => setItems((prev) => prev.filter((it) => it.id !== id)); + const clearAll = () => setItems([]); + + const openEdit = (item: ImageItem) => { + const canvas = renderFullRes(item.image, item.state, watermarkImg); + setEditing({ dataUrl: canvasToDataUrl(canvas), fileName: item.fileName }); + setModalOpen(true); + }; + + const exportItem = (item: ImageItem) => { + const canvas = renderFullRes(item.image, item.state, watermarkImg); + const base = item.fileName.replace(/\.\w+$/, ''); + downloadCanvas(canvas, `${base}${item.state.type ? '_watermark' : ''}.png`); + }; + + return ( +
+ {/* 水印设置 */} +
+
水印设置
+
+
+ 水印类型 + setWmType(e.target.value as WatermarkKind)} + optionType="button" + > + 图片水印 + 文字水印 + +
+ + {wmType === 'text' ? ( +
+ 水印文字 + setWmText(e.target.value)} + style={{ width: 200 }} + /> +
+ ) : ( +
+ 水印图片 + 水印 + 默认水印图,贴图后为圆形 +
+ )} + +
+ 透明度 {wmOpacity}% + +
+
+
+ + {/* 上传 */} +
+ +

+ +

+

点击或拖拽图片到此处上传(支持多张)

+

支持 JPG、PNG、WEBP 格式

+
+
+ + {/* 预览 */} +
+
+
预览(处理后)
+ + + + +
+ + {items.length === 0 ? ( + + ) : ( + <> +
+ {items.map((item) => ( +
+ updateItemState(item.id, { pos })} + /> +
+ {item.fileName} +
+
+ + +
+
+ ))} +
+

添加水印后,可在预览图上按住水印拖动调整位置

+ + )} +
+ + setModalOpen(false)} + /> +
+ ); +}; + +export default AiImagePage; diff --git a/studio/src/pages/ai-image/components/AnnotationCanvas.tsx b/studio/src/pages/ai-image/components/AnnotationCanvas.tsx new file mode 100644 index 0000000..6c38e21 --- /dev/null +++ b/studio/src/pages/ai-image/components/AnnotationCanvas.tsx @@ -0,0 +1,191 @@ +import { useEffect, useRef } from 'react'; +import type { PointerEvent as ReactPointerEvent } from 'react'; +import type { AnnotationElement } from '@/types/annotation'; +import { + drawElement, + drawSelectionHandles, + hitTest, + type HandleType, +} from '@/utils/annotation'; + +const PREVIEW_MAX_WIDTH = 900; + +interface Interaction { + action: 'move' | 'rotate' | 'resize'; + elemId: number; + moved: boolean; + grabDX?: number; + grabDY?: number; + startAngleRad?: number; + startRotation?: number; + startElem?: AnnotationElement; + startDist?: number; +} + +interface AnnotationCanvasProps { + baseImage: HTMLImageElement | null; + elements: AnnotationElement[]; + selectedId: number | null; + onSelect: (id: number | null) => void; + onChange: (next: AnnotationElement[]) => void; + /** 一次交互(拖拽/旋转/拉伸)开始时触发一次,父组件用于存撤销快照 */ + onBeginInteraction: () => void; +} + +const AnnotationCanvas = ({ + baseImage, + elements, + selectedId, + onSelect, + onChange, + onBeginInteraction, +}: AnnotationCanvasProps) => { + const canvasRef = useRef(null); + const interactionRef = useRef(null); + + const scale = () => { + const canvas = canvasRef.current; + if (!canvas || !baseImage) return 1; + return canvas.width / baseImage.width; + }; + + const draw = () => { + const canvas = canvasRef.current; + if (!canvas || !baseImage) return; + const previewW = Math.min(baseImage.width, PREVIEW_MAX_WIDTH); + canvas.width = previewW; + canvas.height = Math.round((baseImage.height / baseImage.width) * previewW); + const ctx = canvas.getContext('2d')!; + const s = canvas.width / baseImage.width; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(baseImage, 0, 0, canvas.width, canvas.height); + elements.forEach((el) => drawElement(ctx, el, s)); + if (selectedId != null) { + const sel = elements.find((el) => el.id === selectedId); + if (sel) drawSelectionHandles(ctx, sel, s); + } + }; + + useEffect(() => { + draw(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [baseImage, elements, selectedId]); + + const toImageCoords = (e: ReactPointerEvent) => { + const canvas = canvasRef.current!; + const rect = canvas.getBoundingClientRect(); + const k = baseImage!.width / rect.width; + return { x: (e.clientX - rect.left) * k, y: (e.clientY - rect.top) * k }; + }; + + const findElementAt = ( + px: number, + py: number, + ): { el: AnnotationElement; handle: HandleType } | null => { + for (let i = elements.length - 1; i >= 0; i--) { + const el = elements[i]; + const handle = hitTest(el, px, py, scale()); + if (handle) return { el, handle }; + } + return null; + }; + + const handlePointerDown = (e: ReactPointerEvent) => { + if (!baseImage) return; + const p = toImageCoords(e); + const hit = findElementAt(p.x, p.y); + if (!hit) { + onSelect(null); + return; + } + onSelect(hit.el.id); + e.currentTarget.setPointerCapture(e.pointerId); + e.preventDefault(); + + if (hit.handle === 'body') { + interactionRef.current = { + action: 'move', + elemId: hit.el.id, + moved: false, + grabDX: p.x - hit.el.x, + grabDY: p.y - hit.el.y, + }; + } else if (hit.handle === 'rotate') { + interactionRef.current = { + action: 'rotate', + elemId: hit.el.id, + moved: false, + startAngleRad: Math.atan2(p.y - hit.el.y, p.x - hit.el.x), + startRotation: hit.el.rotation, + }; + } else { + // 四角圆点缩放 + interactionRef.current = { + action: 'resize', + elemId: hit.el.id, + moved: false, + startElem: hit.el, + startDist: Math.hypot(p.x - hit.el.x, p.y - hit.el.y) || 1, + }; + } + }; + + const handlePointerMove = (e: ReactPointerEvent) => { + const it = interactionRef.current; + if (!it) return; + const p = toImageCoords(e); + if (!it.moved) { + onBeginInteraction(); + it.moved = true; + } + const next = elements.map((el) => { + if (el.id !== it.elemId) return el; + if (it.action === 'move') { + return { ...el, x: p.x - (it.grabDX ?? 0), y: p.y - (it.grabDY ?? 0) }; + } + if (it.action === 'rotate') { + const angle = Math.atan2(p.y - el.y, p.x - el.x); + const delta = (angle - (it.startAngleRad ?? 0)) * (180 / Math.PI); + return { ...el, rotation: (it.startRotation ?? 0) + delta }; + } + const se = it.startElem!; + const raw = Math.hypot(p.x - se.x, p.y - se.y) / (it.startDist || 1); + const k = Math.min(10, Math.max(0.1, raw)); + if (se.type === 'text') { + return { ...se, fontSize: Math.max(6, Math.round(se.fontSize * k)) }; + } + return { + ...se, + length: se.length * k, + tSize: se.tSize * k, + lineWidth: Math.max(1, se.lineWidth * k), + labelFontSize: Math.max(6, se.labelFontSize * k), + }; + }); + onChange(next); + e.preventDefault(); + }; + + const stopDrag = (e: ReactPointerEvent) => { + if (!interactionRef.current) return; + interactionRef.current = null; + if (canvasRef.current?.hasPointerCapture(e.pointerId)) { + canvasRef.current.releasePointerCapture(e.pointerId); + } + }; + + return ( +
+ +
+ ); +}; + +export default AnnotationCanvas; diff --git a/studio/src/pages/ai-image/components/ElementPropsPanel.tsx b/studio/src/pages/ai-image/components/ElementPropsPanel.tsx new file mode 100644 index 0000000..5a3d374 --- /dev/null +++ b/studio/src/pages/ai-image/components/ElementPropsPanel.tsx @@ -0,0 +1,166 @@ +import { ColorPicker, Empty, Input, InputNumber, Select, Slider, Switch } from 'antd'; +import type { AnnotationElement } from '@/types/annotation'; +import { FONT_FAMILIES } from '@/utils/annotation'; + +interface ElementPropsPanelProps { + element: AnnotationElement | null; + onUpdate: (id: number, patch: Partial) => void; +} + +const FONT_OPTIONS = FONT_FAMILIES.map((f) => ({ value: f, label: f })); + +const ElementPropsPanel = ({ element, onUpdate }: ElementPropsPanelProps) => { + if (!element) { + return ( +
+ +

点击画布上的文字或标尺进行编辑

+
+ ); + } + + const update = (patch: Partial) => onUpdate(element.id, patch); + + return ( +
+ {element.type === 'text' ? ( + <> +
+ 内容 + update({ content: e.target.value })} + /> +
+
+ 字体 + update({ label: e.target.value })} /> +
+
+ 标签颜色 + update({ labelColor: c.toHexString() })} + /> +
+
+ 标签字号 + update({ labelFontSize: v ?? 14 })} + style={{ flex: 1 }} + /> +
+ + )} + +
+ 透明度 + update({ opacity: v })} + style={{ flex: 1 }} + /> +
+
+ 旋转(°) + update({ rotation: (v ?? 0) % 360 })} + style={{ flex: 1 }} + /> +
+
+ ); +}; + +export default ElementPropsPanel; diff --git a/studio/src/pages/ai-image/components/ImageEditModal.tsx b/studio/src/pages/ai-image/components/ImageEditModal.tsx new file mode 100644 index 0000000..b09b1a3 --- /dev/null +++ b/studio/src/pages/ai-image/components/ImageEditModal.tsx @@ -0,0 +1,479 @@ +import { useEffect, useRef, useState } from 'react'; +import { App, Button, Drawer, Empty, Input, Select, Spin, Tabs } from 'antd'; +import { + ClearOutlined, + ColumnHeightOutlined, + DeleteOutlined, + DownloadOutlined, + ExperimentOutlined, + FontSizeOutlined, + UndoOutlined, +} from '@ant-design/icons'; +import type { AnnotationElement } from '@/types/annotation'; +import { createRulerElement, createTextElement, drawElement } from '@/utils/annotation'; +import { downloadCanvas } from '@/utils/image'; +import { editImage } from '@/services/image'; +import { apiErrorMessage } from '@/services/api'; +import AnnotationCanvas from './AnnotationCanvas'; +import ElementPropsPanel from './ElementPropsPanel'; + +const MODEL_OPTIONS = [ + { value: 'wanx2.1-imageedit', label: 'wanx2.1-imageedit|通用编辑,加文字弱' }, + { value: 'wan2.6-image', label: 'wan2.6-image|全能图文,效果均衡' }, + { value: 'qwen-image-edit', label: 'qwen-image-edit|精准中英文字' }, + { value: 'qwen-image-edit-plus', label: 'qwen-image-edit-plus|精准文字·更快' }, + { + value: 'qwen-image-edit-plus-2025-10-30', + label: 'qwen-image-edit-plus-2025-10-30|精准文字·稳定', + }, +]; + +type EditTab = 'text' | 'ruler'; + +const PRESETS_KEY = 'ozon_annotation_presets_v1'; + +interface PresetItem { + id: string; + type: EditTab; + name: string; + config: Record; +} + +/** 从元素中提取可复用的「样式配置」(不含内容文字与位置/旋转) */ +function elementToConfig(el: AnnotationElement): Record { + if (el.type === 'text') { + return { + fontFamily: el.fontFamily, + fontSize: el.fontSize, + color: el.color, + strokeColor: el.strokeColor, + strokeWidth: el.strokeWidth, + bold: el.bold, + opacity: el.opacity, + }; + } + return { + length: el.length, + lineColor: el.lineColor, + lineWidth: el.lineWidth, + tSize: el.tSize, + labelColor: el.labelColor, + labelFontSize: el.labelFontSize, + opacity: el.opacity, + }; +} + +interface AiResult { + id: string; + image: HTMLImageElement; + dataUrl: string; + url: string; +} + +interface ImageEditModalProps { + open: boolean; + imageDataUrl: string; + fileName: string; + onClose: () => void; +} + +const ImageEditModal = ({ open, imageDataUrl, fileName, onClose }: ImageEditModalProps) => { + const { message } = App.useApp(); + const [originalImage, setOriginalImage] = useState(null); + const [aiResults, setAiResults] = useState([]); + const [currentKey, setCurrentKey] = useState('original'); + const [elements, setElements] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [editTab, setEditTab] = useState('text'); + const [undoStack, setUndoStack] = useState([]); + const [aiPrompt, setAiPrompt] = useState(''); + const [aiLoading, setAiLoading] = useState(false); + const [model, setModel] = useState('wanx2.1-imageedit'); + const [presets, setPresets] = useState([]); + const [selectedPreset, setSelectedPreset] = useState>({ + text: null, + ruler: null, + }); + const seqRef = useRef(0); + + // 每次打开重置 + useEffect(() => { + if (!open) return; + setAiResults([]); + setCurrentKey('original'); + setElements([]); + setSelectedId(null); + setEditTab('text'); + setUndoStack([]); + setAiPrompt(''); + setAiLoading(false); + const img = new Image(); + img.onload = () => setOriginalImage(img); + img.onerror = () => message.error('原图加载失败'); + img.src = imageDataUrl; + }, [open, imageDataUrl, message]); + + // 加载已保存的预设 + useEffect(() => { + try { + const raw = localStorage.getItem(PRESETS_KEY); + if (raw) setPresets(JSON.parse(raw) || []); + } catch { + /* ignore */ + } + }, []); + + const baseImage = + currentKey === 'original' + ? originalImage + : aiResults.find((r) => r.id === currentKey)?.image ?? originalImage; + + const currentDataUrl = + currentKey === 'original' + ? imageDataUrl + : aiResults.find((r) => r.id === currentKey)?.dataUrl ?? imageDataUrl; + + const pushUndo = () => setUndoStack((prev) => [...prev, elements]); + + const undo = () => { + if (undoStack.length === 0) return; + const last = undoStack[undoStack.length - 1]; + setElements(last); + setUndoStack(undoStack.slice(0, -1)); + setSelectedId(null); + }; + + const addText = () => { + if (!baseImage) return; + pushUndo(); + const base = createTextElement(seqRef.current++, baseImage.width, baseImage.height); + const preset = presets.find((p) => p.id === selectedPreset.text); + const el = preset ? ({ ...base, ...preset.config } as AnnotationElement) : base; + setElements((prev) => [...prev, el]); + setSelectedId(el.id); + setEditTab('text'); + }; + + const addRuler = () => { + if (!baseImage) return; + pushUndo(); + const base = createRulerElement(seqRef.current++, baseImage.width, baseImage.height); + const preset = presets.find((p) => p.id === selectedPreset.ruler); + const el = preset ? ({ ...base, ...preset.config } as AnnotationElement) : base; + setElements((prev) => [...prev, el]); + setSelectedId(el.id); + setEditTab('ruler'); + }; + + const clearAnnotations = () => { + if (elements.length === 0) return; + pushUndo(); + setElements([]); + setSelectedId(null); + }; + + const onUpdate = (id: number, patch: Partial) => { + setElements((prev) => + prev.map((el) => (el.id === id ? ({ ...el, ...patch } as AnnotationElement) : el)), + ); + }; + + const onDelete = (id: number) => { + pushUndo(); + setElements((prev) => prev.filter((el) => el.id !== id)); + setSelectedId((cur) => (cur === id ? null : cur)); + }; + + const handleSelect = (id: number | null) => { + setSelectedId(id); + if (id != null) { + const el = elements.find((e) => e.id === id); + if (el) setEditTab(el.type); + } + }; + + const runAi = async () => { + const prompt = aiPrompt.trim(); + if (!prompt || aiLoading) return; + setAiLoading(true); + try { + const res = await editImage({ + base_image: currentDataUrl, + prompt, + model, + n: 1, + strength: 0.8, + prompt_extend: true, + }); + const r = res.results[0]; + const dataUrl = r?.image_base64 || r?.url; + if (!r || !dataUrl) throw new Error('模型未返回图片'); + const img = new Image(); + img.onload = () => { + const id = `ai-${Date.now()}`; + setAiResults((prev) => [...prev, { id, image: img, dataUrl, url: r.url }]); + setCurrentKey(id); + setAiLoading(false); + setAiPrompt(''); + }; + img.onerror = () => { + setAiLoading(false); + message.error('AI 结果图加载失败'); + }; + img.src = dataUrl; + } catch (error) { + setAiLoading(false); + message.error(apiErrorMessage(error)); + } + }; + + const exportImage = () => { + if (!baseImage) return; + const canvas = document.createElement('canvas'); + canvas.width = baseImage.width; + canvas.height = baseImage.height; + const ctx = canvas.getContext('2d')!; + ctx.drawImage(baseImage, 0, 0); + elements.forEach((el) => drawElement(ctx, el, 1)); + const base = fileName.replace(/\.\w+$/, ''); + downloadCanvas(canvas, `${base}_edited.png`); + }; + + const selectedElement = elements.find((el) => el.id === selectedId) ?? null; + const hasSelection = selectedElement != null && selectedElement.type === editTab; + const deleteSelected = () => { + if (selectedElement) onDelete(selectedElement.id); + }; + + const persistPresets = (next: PresetItem[]) => { + setPresets(next); + try { + localStorage.setItem(PRESETS_KEY, JSON.stringify(next)); + } catch { + /* ignore */ + } + }; + + const savePreset = () => { + if (!selectedElement || selectedElement.type !== editTab) return; + const type = selectedElement.type; + const count = presets.filter((p) => p.type === type).length; + const item: PresetItem = { + id: `${type}-${Date.now()}`, + type, + name: `${type === 'text' ? '文字' : '标尺'}预设 ${count + 1}`, + config: elementToConfig(selectedElement), + }; + persistPresets([...presets, item]); + setSelectedPreset((prev) => ({ ...prev, [type]: item.id })); + message.success(`已保存「${item.name}」`); + }; + + const applyPreset = (presetId?: string) => { + setSelectedPreset((prev) => ({ ...prev, [editTab]: presetId ?? null })); + if (!presetId) return; + const preset = presets.find((p) => p.id === presetId); + if (!preset) return; + if (selectedElement && selectedElement.type === editTab) { + onUpdate(selectedElement.id, preset.config as Partial); + } + }; + + return ( + +
+
+ {/* 左:底图切换 */} +
+ setCurrentKey('original')} + /> + {aiResults.map((r) => ( + setCurrentKey(r.id)} + /> + ))} + {aiResults.length === 0 && ( +
AI 生成结果会出现在这里,点击可切换为底图
+ )} +
+ + {/* 中:画布 */} +
+ {baseImage ? ( + + ) : ( +
+ +
+ )} +
+ + {/* 右:标注编辑区 */} +
+ setEditTab(k as EditTab)} + size="small" + className="wb-tabs" + items={[ + { key: 'text', label: '文字' }, + { key: 'ruler', label: '标尺' }, + ]} + /> +
+ {editTab === 'text' ? ( + + ) : ( + + )} + +
+
+ + +
+
+
+
+
+ ); +}; + +function Thumb({ + label, + active, + src, + onClick, +}: { + label: string; + active: boolean; + src: string; + onClick: () => void; +}) { + return ( +
+ {label} + {label} +
+ ); +} + +export default ImageEditModal; diff --git a/studio/src/pages/ai-image/components/WatermarkCanvas.tsx b/studio/src/pages/ai-image/components/WatermarkCanvas.tsx new file mode 100644 index 0000000..479673d --- /dev/null +++ b/studio/src/pages/ai-image/components/WatermarkCanvas.tsx @@ -0,0 +1,99 @@ +import { useEffect, useRef } from 'react'; +import type { PointerEvent as ReactPointerEvent } from 'react'; +import type { ImageItem } from '@/types/image'; +import { clampPos, drawWatermarked, getWatermarkSize } from '@/utils/watermark'; + +const PREVIEW_MAX_WIDTH = 360; + +interface WatermarkCanvasProps { + item: ImageItem; + watermarkImg: HTMLImageElement | null; + onPosChange: (pos: { x: number; y: number }) => void; +} + +/** + * 单张图片的可拖拽水印预览画布。 + * 水印坐标始终相对原图;预览按 PREVIEW_MAX_WIDTH 等比绘制。 + */ +const WatermarkCanvas = ({ item, watermarkImg, onPosChange }: WatermarkCanvasProps) => { + const canvasRef = useRef(null); + const posRef = useRef(item.state.pos); + const dragRef = useRef<{ grabX: number; grabY: number } | null>(null); + + const draw = () => { + const canvas = canvasRef.current; + if (!canvas) return; + drawWatermarked(canvas, item.image, { + state: { ...item.state, pos: posRef.current }, + watermarkImg, + targetWidth: PREVIEW_MAX_WIDTH, + }); + }; + + useEffect(() => { + posRef.current = item.state.pos; + draw(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [item.state, watermarkImg]); + + const toImageCoords = (e: ReactPointerEvent) => { + const canvas = canvasRef.current!; + const rect = canvas.getBoundingClientRect(); + const scale = item.image.width / rect.width; + return { x: (e.clientX - rect.left) * scale, y: (e.clientY - rect.top) * scale }; + }; + + const handlePointerDown = (e: ReactPointerEvent) => { + if (!item.state.type) return; + if (item.state.type === 'image' && !watermarkImg) return; + + const point = toImageCoords(e); + const size = getWatermarkSize(item.image.width, { ...item.state, pos: posRef.current }); + const inside = + point.x >= posRef.current.x && + point.x <= posRef.current.x + size.width && + point.y >= posRef.current.y && + point.y <= posRef.current.y + size.height; + if (!inside) return; + + dragRef.current = { grabX: point.x - posRef.current.x, grabY: point.y - posRef.current.y }; + e.currentTarget.setPointerCapture(e.pointerId); + e.preventDefault(); + }; + + const handlePointerMove = (e: ReactPointerEvent) => { + const drag = dragRef.current; + if (!drag) return; + const point = toImageCoords(e); + const next = clampPos( + item.image.width, + item.image.height, + point.x - drag.grabX, + point.y - drag.grabY, + item.state, + ); + posRef.current = next; + draw(); + onPosChange(next); + e.preventDefault(); + }; + + const stopDrag = (e: ReactPointerEvent) => { + if (!dragRef.current) return; + dragRef.current = null; + e.currentTarget.releasePointerCapture(e.pointerId); + }; + + return ( + + ); +}; + +export default WatermarkCanvas; diff --git a/studio/src/pages/ai-image/index.ts b/studio/src/pages/ai-image/index.ts new file mode 100644 index 0000000..8c479b4 --- /dev/null +++ b/studio/src/pages/ai-image/index.ts @@ -0,0 +1 @@ +export { default } from './AiImagePage'; diff --git a/studio/src/router/index.tsx b/studio/src/router/index.tsx new file mode 100644 index 0000000..2d48512 --- /dev/null +++ b/studio/src/router/index.tsx @@ -0,0 +1,15 @@ +import { createBrowserRouter, Navigate } from 'react-router'; +import MainLayout from '@/layouts/MainLayout'; +import AiImagePage from '@/pages/ai-image'; + +export const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'ai-image', element: }, + ], + }, + { path: '*', element: }, +]); diff --git a/studio/src/services/api.ts b/studio/src/services/api.ts new file mode 100644 index 0000000..feb4427 --- /dev/null +++ b/studio/src/services/api.ts @@ -0,0 +1,48 @@ +import axios from 'axios'; +import type { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { envConfig } from '@/config/env'; + +const apiClient: AxiosInstance = axios.create({ + baseURL: envConfig.apiBaseUrl, + timeout: 120000, // 图生图较慢,放宽到 120s + headers: { + 'Content-Type': 'application/json', + }, +}); + +apiClient.interceptors.response.use( + (response) => response, + (error) => { + if (envConfig.debug) { + console.error('请求失败:', error.response?.data || error.message); + } + return Promise.reject(error); + }, +); + +export const api = { + get: (url: string, config?: AxiosRequestConfig) => + apiClient.get(url, config).then((res) => res.data), + + post: (url: string, data?: unknown, config?: AxiosRequestConfig) => + apiClient.post(url, data, config).then((res) => res.data), +}; + +/** 从异常中提取后端返回的错误信息(FastAPI 的 HTTPException detail) */ +export function apiErrorMessage(error: unknown): string { + if (axios.isAxiosError(error)) { + const data = error.response?.data as { detail?: unknown } | undefined; + if (data?.detail) { + if (typeof data.detail === 'string') return data.detail; + try { + return JSON.stringify(data.detail); + } catch { + return String(data.detail); + } + } + return error.message; + } + return error instanceof Error ? error.message : String(error); +} + +export default apiClient; diff --git a/studio/src/services/image.ts b/studio/src/services/image.ts new file mode 100644 index 0000000..c17e396 --- /dev/null +++ b/studio/src/services/image.ts @@ -0,0 +1,7 @@ +import { api } from './api'; +import type { ImageEditRequest, ImageEditResponse } from '@/types/image'; + +/** 调用后端图生图接口(wanx2.1-imageedit) */ +export function editImage(payload: ImageEditRequest): Promise { + return api.post('/image/edit', payload); +} diff --git a/studio/src/types/annotation.ts b/studio/src/types/annotation.ts new file mode 100644 index 0000000..2e643d2 --- /dev/null +++ b/studio/src/types/annotation.ts @@ -0,0 +1,43 @@ +/** 图片标注元素模型(精确文字叠加 + 标尺) */ + +export type AnnotationElementType = 'text' | 'ruler'; + +export interface BaseAnnotationElement { + id: number; + type: AnnotationElementType; + /** 中心点,相对原图坐标 */ + x: number; + y: number; + /** 旋转角度(度) */ + rotation: number; + /** 透明度 0~1 */ + opacity: number; +} + +export interface TextElement extends BaseAnnotationElement { + type: 'text'; + content: string; + fontFamily: string; + /** 字号(原图像素) */ + fontSize: number; + color: string; + strokeColor: string; + /** 描边宽度(原图像素),0 = 无描边 */ + strokeWidth: number; + bold: boolean; +} + +export interface RulerElement extends BaseAnnotationElement { + type: 'ruler'; + /** 标尺长度(原图像素) */ + length: number; + lineColor: string; + lineWidth: number; + /** 两端 T 字短横总长(原图像素) */ + tSize: number; + label: string; + labelColor: string; + labelFontSize: number; +} + +export type AnnotationElement = TextElement | RulerElement; diff --git a/studio/src/types/image.ts b/studio/src/types/image.ts new file mode 100644 index 0000000..e8977ee --- /dev/null +++ b/studio/src/types/image.ts @@ -0,0 +1,57 @@ +/** 水印类型:'image' 图片水印,'text' 文字水印,null 无水印 */ +export type WatermarkType = 'image' | 'text' | null; + +export interface WatermarkPos { + x: number; + y: number; +} + +/** 单张图片的水印状态(坐标均相对原图) */ +export interface WatermarkState { + type: WatermarkType; + text: string; + opacity: number; // 0 ~ 100 + pos: WatermarkPos; +} + +/** 图片列表项 */ +export interface ImageItem { + id: number; + fileName: string; + /** 原始图片 */ + image: HTMLImageElement; + /** 原始图片 data URL */ + dataUrl: string; + state: WatermarkState; +} + +/** 图生图请求体 */ +export interface ImageEditRequest { + base_image: string; + prompt: string; + /** 模型 code,见后端 SUPPORTED_MODELS */ + model?: string; + mask_image?: string; + function?: 'description_edit' | 'description_edit_with_mask' | 'stylization_local' | 'stylization_all'; + n?: number; + size?: string; + seed?: number; + style?: string; + prompt_extend?: boolean; + /** 修改幅度 0.0~1.0,越大改动越明显;新增文字等建议 0.8 左右 */ + strength?: number; +} + +/** 图生图结果 */ +export interface ImageEditResult { + url: string; + /** 服务端代理下载的 data URL,便于前端合成/导出(规避跨域画布污染) */ + image_base64?: string; +} + +export interface ImageEditResponse { + task_id: string; + results: ImageEditResult[]; + image_count: number; + request_id: string; +} diff --git a/studio/src/utils/annotation.ts b/studio/src/utils/annotation.ts new file mode 100644 index 0000000..4a800e4 --- /dev/null +++ b/studio/src/utils/annotation.ts @@ -0,0 +1,233 @@ +/** 标注元素的绘制、测量、命中检测与工厂函数 */ +import type { AnnotationElement, RulerElement, TextElement } from '@/types/annotation'; + +export const FONT_FAMILIES = [ + 'PingFang SC', + 'Microsoft YaHei', + 'SimSun', + 'Songti SC', + 'Arial', + 'Helvetica', + 'Georgia', + 'Times New Roman', + 'Courier New', +]; + +// 屏幕像素下的手柄常量(绘制与命中检测时按 scale 换算到原图坐标) +export const HANDLE_RADIUS = 7; // 手柄半径 +export const ROTATE_GAP = 26; // 旋转手柄距元素包围盒顶部的间距 + +const measureCtx = document.createElement('canvas').getContext('2d')!; + +export function textFont(elem: TextElement, scale = 1): string { + return `${elem.bold ? 'bold ' : ''}${Math.round(elem.fontSize * scale)}px ${elem.fontFamily}`; +} + +export function measureText(elem: TextElement): { width: number; height: number } { + measureCtx.font = textFont(elem, 1); + const lines = (elem.content || '').split('\n'); + let width = 0; + for (const line of lines) width = Math.max(width, measureCtx.measureText(line).width); + return { width, height: elem.fontSize * 1.2 * lines.length }; +} + +/** 元素包围盒半宽半高(原图坐标),用于画选中框与旋转手柄定位 */ +export function elementBox(elem: AnnotationElement): { halfW: number; halfH: number } { + if (elem.type === 'text') { + const { width, height } = measureText(elem); + return { halfW: width / 2 + 4, halfH: height / 2 + 4 }; + } + const half = elem.length / 2; + return { + halfW: half + elem.tSize / 2 + 4, + halfH: Math.max(elem.tSize / 2, elem.lineWidth / 2 + 4) + 4, + }; +} + +/** 世界坐标 → 元素局部坐标(逆旋转 + 平移) */ +export function toLocal(elem: AnnotationElement, px: number, py: number): { lx: number; ly: number } { + const rad = (-elem.rotation * Math.PI) / 180; + const dx = px - elem.x; + const dy = py - elem.y; + return { + lx: dx * Math.cos(rad) - dy * Math.sin(rad), + ly: dx * Math.sin(rad) + dy * Math.cos(rad), + }; +} + +export type HandleType = 'body' | 'rotate' | 'tl' | 'tr' | 'bl' | 'br'; + +export function hitTest(elem: AnnotationElement, px: number, py: number, scale: number): HandleType | null { + const { lx, ly } = toLocal(elem, px, py); + const r = HANDLE_RADIUS / scale; // 原图坐标下的手柄半径 + const { halfW, halfH } = elementBox(elem); + const rotateY = -(halfH + ROTATE_GAP / scale); // 旋转手柄在局部坐标的 y + if (Math.hypot(lx, ly - rotateY) <= r * 1.6) return 'rotate'; + + // 四角缩放圆点 + const corners: [HandleType, number, number][] = [ + ['tl', -halfW, -halfH], + ['tr', halfW, -halfH], + ['bl', -halfW, halfH], + ['br', halfW, halfH], + ]; + for (const [name, cx, cy] of corners) { + if (Math.hypot(lx - cx, ly - cy) <= r * 1.6) return name; + } + + if (elem.type === 'ruler') { + const half = elem.length / 2; + const hitHalfW = half + elem.tSize / 2; + const hitHalfH = Math.max(elem.lineWidth / 2, 4) + 4; + if (Math.abs(lx) <= hitHalfW && Math.abs(ly) <= hitHalfH) return 'body'; + return null; + } + + const { width, height } = measureText(elem); + if (Math.abs(lx) <= width / 2 + 4 && Math.abs(ly) <= height / 2 + 4) return 'body'; + return null; +} + +export function drawElement(ctx: CanvasRenderingContext2D, elem: AnnotationElement, scale: number): void { + ctx.save(); + ctx.globalAlpha = elem.opacity; + ctx.translate(elem.x * scale, elem.y * scale); + ctx.rotate((elem.rotation * Math.PI) / 180); + if (elem.type === 'text') drawText(ctx, elem, scale); + else drawRuler(ctx, elem, scale); + ctx.restore(); +} + +function drawText(ctx: CanvasRenderingContext2D, elem: TextElement, scale: number): void { + const fontSize = elem.fontSize * scale; + ctx.font = textFont(elem, scale); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.lineJoin = 'round'; + const lines = (elem.content || '').split('\n'); + const lineHeight = fontSize * 1.2; + const startY = -((lines.length - 1) * lineHeight) / 2; + lines.forEach((line, i) => { + const y = startY + i * lineHeight; + if (elem.strokeWidth > 0) { + ctx.lineWidth = elem.strokeWidth * scale; + ctx.strokeStyle = elem.strokeColor; + ctx.strokeText(line, 0, y); + } + ctx.fillStyle = elem.color; + ctx.fillText(line, 0, y); + }); +} + +function drawRuler(ctx: CanvasRenderingContext2D, elem: RulerElement, scale: number): void { + const half = (elem.length / 2) * scale; + const lw = elem.lineWidth * scale; + const t = elem.tSize * scale; + ctx.strokeStyle = elem.lineColor; + ctx.lineWidth = lw; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(-half, 0); + ctx.lineTo(half, 0); + ctx.moveTo(-half, -t / 2); + ctx.lineTo(-half, t / 2); + ctx.moveTo(half, -t / 2); + ctx.lineTo(half, t / 2); + ctx.stroke(); + if (elem.label) { + const fs = elem.labelFontSize * scale; + ctx.font = `${Math.round(fs)}px "PingFang SC", "Microsoft YaHei", sans-serif`; + ctx.fillStyle = elem.labelColor; + ctx.textAlign = 'center'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText(elem.label, 0, -t / 2 - fs * 0.25); + } +} + +/** 绘制选中态:虚线包围盒 + 旋转手柄 +(标尺)端点手柄 */ +export function drawSelectionHandles( + ctx: CanvasRenderingContext2D, + elem: AnnotationElement, + scale: number, +): void { + const { halfW, halfH } = elementBox(elem); + ctx.save(); + ctx.translate(elem.x * scale, elem.y * scale); + ctx.rotate((elem.rotation * Math.PI) / 180); + const w = halfW * 2 * scale; + const h = halfH * 2 * scale; + const r = HANDLE_RADIUS; + + ctx.strokeStyle = '#8b5cf6'; + ctx.lineWidth = 1.5; + ctx.setLineDash([5, 4]); + ctx.strokeRect(-w / 2, -h / 2, w, h); + ctx.setLineDash([]); + + const rotY = -(h / 2 + ROTATE_GAP); + ctx.beginPath(); + ctx.moveTo(0, -h / 2); + ctx.lineTo(0, rotY); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, rotY, r, 0, Math.PI * 2); + ctx.fillStyle = '#8b5cf6'; + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2; + ctx.stroke(); + + // 四角缩放圆点 + const corners = [ + [-w / 2, -h / 2], + [w / 2, -h / 2], + [-w / 2, h / 2], + [w / 2, h / 2], + ]; + for (const [cx, cy] of corners) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2); + ctx.fillStyle = '#fff'; + ctx.fill(); + ctx.strokeStyle = '#8b5cf6'; + ctx.lineWidth = 2; + ctx.stroke(); + } + ctx.restore(); +} + +export function createTextElement(id: number, imgW: number, imgH: number): TextElement { + return { + id, + type: 'text', + x: imgW / 2, + y: imgH / 2, + rotation: 0, + opacity: 1, + content: '双击或右侧编辑文字', + fontFamily: 'PingFang SC', + fontSize: Math.max(18, Math.round(imgW * 0.06)), + color: '#ffffff', + strokeColor: '#000000', + strokeWidth: 0, + bold: false, + }; +} + +export function createRulerElement(id: number, imgW: number, imgH: number): RulerElement { + return { + id, + type: 'ruler', + x: imgW / 2, + y: imgH / 2, + rotation: 0, + opacity: 1, + length: Math.round(imgW * 0.5), + lineColor: '#ff4d4f', + lineWidth: Math.max(2, Math.round(imgW / 300)), + tSize: Math.max(14, Math.round(imgW * 0.04)), + label: '', + labelColor: '#ff4d4f', + labelFontSize: Math.max(14, Math.round(imgW * 0.035)), + }; +} diff --git a/studio/src/utils/image.ts b/studio/src/utils/image.ts new file mode 100644 index 0000000..a008132 --- /dev/null +++ b/studio/src/utils/image.ts @@ -0,0 +1,37 @@ +/** 图片文件读取与导出辅助 */ + +export interface LoadedImage { + image: HTMLImageElement; + dataUrl: string; +} + +/** 读取 File 为 HTMLImageElement 与 data URL */ +export function fileToImage(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = reader.result as string; + const image = new Image(); + image.onload = () => resolve({ image, dataUrl }); + image.onerror = () => reject(new Error('图片加载失败')); + image.src = dataUrl; + }; + reader.onerror = () => reject(new Error('文件读取失败')); + reader.readAsDataURL(file); + }); +} + +/** canvas 转 data URL(PNG) */ +export function canvasToDataUrl(canvas: HTMLCanvasElement): string { + return canvas.toDataURL('image/png'); +} + +/** 下载单个 canvas 为 PNG 文件 */ +export function downloadCanvas(canvas: HTMLCanvasElement, fileName: string): void { + const link = document.createElement('a'); + link.href = canvas.toDataURL('image/png'); + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} diff --git a/studio/src/utils/watermark.ts b/studio/src/utils/watermark.ts new file mode 100644 index 0000000..70aa2f3 --- /dev/null +++ b/studio/src/utils/watermark.ts @@ -0,0 +1,145 @@ +/** + * 水印绘制能力(移植自 web/js/app.js,去掉了 web v1 的“图片水印缩图”行为)。 + * 所有坐标均以原图为基准;预览/导出时按 targetWidth 等比缩放绘制。 + */ +import type { WatermarkState } from '@/types/image'; + +export const WATERMARK_SCALE = 0.15; // 图片水印直径占原图宽度的比例 +export const WATERMARK_MARGIN = 10; // 默认位置距右下角的间距(原图像素) +export const WATERMARK_TEXT_SCALE = 0.051; // 文字水印字号占原图宽度的比例(原 0.06 的 85%) + +interface Size { + width: number; + height: number; +} + +const measureCtx = document.createElement('canvas').getContext('2d')!; + +function watermarkFont(fontSize: number): string { + return `bold ${fontSize}px "PingFang SC", "Microsoft YaHei", Arial, sans-serif`; +} + +function fontSizeFor(imgWidth: number): number { + return Math.max(12, Math.round(imgWidth * WATERMARK_TEXT_SCALE)); +} + +/** 水印在原图坐标系中的尺寸 */ +export function getWatermarkSize(imgWidth: number, state: WatermarkState): Size { + if (state.type === 'text') { + const fontSize = fontSizeFor(imgWidth); + measureCtx.font = watermarkFont(fontSize); + return { width: measureCtx.measureText(state.text).width, height: fontSize * 1.25 }; + } + const diameter = imgWidth * WATERMARK_SCALE; + return { width: diameter, height: diameter }; +} + +/** 限制水印不被拖出图片范围 */ +export function clampPos( + imgWidth: number, + imgHeight: number, + x: number, + y: number, + state: WatermarkState, +): { x: number; y: number } { + const size = getWatermarkSize(imgWidth, state); + const maxX = Math.max(0, imgWidth - size.width); + const maxY = Math.max(0, imgHeight - size.height); + return { + x: Math.min(Math.max(0, x), maxX), + y: Math.min(Math.max(0, y), maxY), + }; +} + +/** 默认位置:右下角 */ +export function defaultPos(imgWidth: number, imgHeight: number, state: WatermarkState): { x: number; y: number } { + const size = getWatermarkSize(imgWidth, state); + return clampPos( + imgWidth, + imgHeight, + imgWidth - size.width - WATERMARK_MARGIN, + imgHeight - size.height - WATERMARK_MARGIN, + state, + ); +} + +interface DrawOptions { + state: WatermarkState; + watermarkImg: HTMLImageElement | null; + targetWidth: number; +} + +/** 把原图 + 水印合成到指定 canvas(targetWidth 为输出宽度,等比缩放) */ +export function drawWatermarked( + canvas: HTMLCanvasElement, + source: HTMLImageElement | HTMLCanvasElement, + { state, watermarkImg, targetWidth }: DrawOptions, +): HTMLCanvasElement { + const scale = targetWidth / source.width; + canvas.width = Math.max(1, Math.round(targetWidth)); + canvas.height = Math.max(1, Math.round(source.height * scale)); + const ctx = canvas.getContext('2d')!; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(source, 0, 0, canvas.width, canvas.height); + + if (state.type === 'image' && watermarkImg) { + const size = getWatermarkSize(source.width, state); + const drawX = state.pos.x * scale; + const drawY = state.pos.y * scale; + const drawSize = size.width * scale; + + const edge = Math.min(watermarkImg.width, watermarkImg.height); + const sx = (watermarkImg.width - edge) / 2; + const sy = (watermarkImg.height - edge) / 2; + + ctx.save(); + ctx.beginPath(); + ctx.arc(drawX + drawSize / 2, drawY + drawSize / 2, drawSize / 2, 0, Math.PI * 2); + ctx.closePath(); + ctx.clip(); + ctx.globalAlpha = state.opacity / 100; + ctx.drawImage(watermarkImg, sx, sy, edge, edge, drawX, drawY, drawSize, drawSize); + ctx.restore(); + } else if (state.type === 'text' && state.text) { + const fontSize = fontSizeFor(source.width) * scale; + const font = watermarkFont(fontSize); + measureCtx.font = font; + const padding = Math.ceil(fontSize / 4); + + // 先画到离屏画布再整体合成,避免透明度让描边透出字面显脏 + const layer = document.createElement('canvas'); + layer.width = Math.ceil(measureCtx.measureText(state.text).width) + padding * 2; + layer.height = Math.ceil(fontSize * 1.5) + padding * 2; + const layerCtx = layer.getContext('2d')!; + layerCtx.font = font; + layerCtx.textBaseline = 'top'; + layerCtx.lineWidth = Math.max(1, fontSize / 8); + layerCtx.lineJoin = 'round'; + layerCtx.strokeStyle = 'rgba(0, 0, 0, 0.55)'; + layerCtx.strokeText(state.text, padding, padding); + layerCtx.fillStyle = '#ffffff'; + layerCtx.fillText(state.text, padding, padding); + + const drawX = state.pos.x * scale; + const drawY = state.pos.y * scale; + ctx.save(); + ctx.globalAlpha = state.opacity / 100; + ctx.drawImage(layer, drawX - padding, drawY + fontSize * 0.1 - padding); + ctx.restore(); + } + + return canvas; +} + +/** 生成全分辨率合成图(用于导出 / 发送给 AI) */ +export function renderFullRes( + source: HTMLImageElement | HTMLCanvasElement, + state: WatermarkState, + watermarkImg: HTMLImageElement | null, +): HTMLCanvasElement { + return drawWatermarked(document.createElement('canvas'), source, { + state, + watermarkImg, + targetWidth: source.width, + }); +} diff --git a/studio/src/vite-env.d.ts b/studio/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/studio/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/studio/tsconfig.app.json b/studio/tsconfig.app.json new file mode 100644 index 0000000..26143f8 --- /dev/null +++ b/studio/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/studio/tsconfig.app.tsbuildinfo b/studio/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..6413cea --- /dev/null +++ b/studio/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/config/env.ts","./src/layouts/mainlayout.tsx","./src/layouts/sidebarmenu.tsx","./src/layouts/menuconfig.tsx","./src/pages/ai-image/aiimagepage.tsx","./src/pages/ai-image/index.ts","./src/pages/ai-image/components/annotationcanvas.tsx","./src/pages/ai-image/components/elementpropspanel.tsx","./src/pages/ai-image/components/imageeditmodal.tsx","./src/pages/ai-image/components/watermarkcanvas.tsx","./src/router/index.tsx","./src/services/api.ts","./src/services/image.ts","./src/types/annotation.ts","./src/types/image.ts","./src/utils/annotation.ts","./src/utils/image.ts","./src/utils/watermark.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/studio/tsconfig.json b/studio/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/studio/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/studio/tsconfig.node.json b/studio/tsconfig.node.json new file mode 100644 index 0000000..c5395d4 --- /dev/null +++ b/studio/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/studio/tsconfig.node.tsbuildinfo b/studio/tsconfig.node.tsbuildinfo new file mode 100644 index 0000000..62c7bf9 --- /dev/null +++ b/studio/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/studio/vite.config.ts b/studio/vite.config.ts new file mode 100644 index 0000000..51ded0f --- /dev/null +++ b/studio/vite.config.ts @@ -0,0 +1,24 @@ +import { fileURLToPath, URL } from 'node:url'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; + +// 前端开发服务器,/api 代理到本地 FastAPI(server/) +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + server: { + port: 8900, + strictPort: true, // 端口被占用时直接报错,而不是自动改到别的端口 + open: true, // 启动后自动打开浏览器 + proxy: { + '/api': { + target: 'http://127.0.0.1:8800', + changeOrigin: true, + }, + }, + }, +});