diff --git a/.gitignore b/.gitignore
index 4be2b89..7b64b68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,6 @@ wheels/
# Logs
*.log
+
+# Frontend
+node_modules/
diff --git a/CHANGELOG b/CHANGELOG
index 57397a4..8d0596b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,8 +1,10 @@
# CHANGELOG
+- [2026-09-20 09:05] FEAT: 前后端全量开发 v0.2——后端新增客户/合同(意向单→正式)/优化下料(FFD)模块与订单合同快照;前端 Vue3+Element Plus 七页面(登录/工作台/订单/合同/客户/样板库/优化下料)与自研二维窗型画布渲染器(缩放/平移/开向标记/尺寸标注);构建产物挂载后端+SPA history 回退;全接口回归 6/6 通过 (Files: src/huamenchuang/**, frontend/**, README.md, doc/API.md)
- [2026-09-20 08:14] FEAT: 订单模块 v0.1 实现与二维平面图全量采集——FastAPI 分层架构(api/services/repositories/models)、订单状态机(意向单→正式单→生产中→完工/取消)、7节点生产进度、审计日志、JWT认证、样板库API;创建契约对齐目标站{contract_id,drawing_data,pic_url};254张样板平面图全部下载成功并静态挂载;端到端冒烟测试7/7通过 (Files: src/huamenchuang/**, pyproject.toml, README.md, doc/API.md, data/windoor-images/*, data/fetch-images.py)
- [2026-09-20 07:18] FEAT: 整站全量采集——GET 扫描 279 个接口全部留档(业务成功76)、POST 安全探测 24 个(付费墙/参数契约确认)、订单写入接口契约经源码静态分析补全(orders CRUD + 创建订单请求体);doc/API.md 新增 §11 采集结果与付费墙语义 (Files: data/full-site/*, doc/API.md, doc/克隆实施方案.md)
- [2026-09-20 06:41] FEAT: 完成目标站逆向采集——样板库 254 套模板 drawing_data 全量采集(254/254)、16 个核心接口响应样本、约 300 条接口清单;输出《克隆实施方案》《API 接口文档》(Files: doc/克隆实施方案.md, doc/API.md, data/api-endpoint-inventory.txt, data/api-samples/*, data/windoor-drawings/*)
- [2026-09-20 06:41] DOCS: 初始化 README 项目导航 (Files: README.md)
+
diff --git a/README.md b/README.md
index a72e580..36d6d0b 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,9 @@
## 当前进度
- ✅ 目标站逆向与数据采集(样板库 254 套全量 + 二维平面图 254 张 + 接口全量留档)
-- ✅ 后端 v0.1:认证 / 订单模块(状态机+生产进度+审计日志)/ 样板库 API
-- ⏳ 前端 Vue3 与画布编辑器(规划中,见方案 §3.3)
+- ✅ 后端 v0.2:认证 / 订单(状态机+生产进度+审计日志)/ 客户 / 合同(意向单→正式单)/ 样板库 / 优化下料(FFD)
+- ✅ 前端 v0.2:Vue3 + Element Plus,7 个页面,自研二维窗型画布渲染器(可渲染全部 254 套样板)
+- ⏳ 报价引擎 / 打印模板 / 微信生态(规划中,见方案 §6 里程碑)
## 文档导航
@@ -31,11 +32,16 @@ frontend/ 前端(Vue3 + TS,规划中)
## 快速开始
```bash
-uv sync # 安装依赖
+uv sync # 后端依赖
+npm --prefix frontend install # 前端依赖
uv run python -m huamenchuang.scripts.seed # 管理员 + 254 套样板入库
-uv run uvicorn huamenchuang.main:app --reload # 启动 → http://localhost:8000/docs
+npm --prefix frontend run build # 构建前端到 frontend/dist(后端自动挂载)
+uv run uvicorn huamenchuang.main:app --reload # 启动 → http://localhost:8000
```
+打开 http://localhost:8000 即为完整系统(SPA 由后端服务,深链接自动回退)。
+前端开发热更新:`npm --prefix frontend run dev`(5173 端口,/api 代理到 8000)。
+
管理员账号(开发):`13800000000 / 123456`。
环境变量(均可选):`HMC_DATABASE_URL`(默认 SQLite,可切 MySQL)、`HMC_JWT_SECRET`(生产必设)、`HMC_ENABLED_MODULES`(套餐模块开关)。
diff --git a/doc/API.md b/doc/API.md
index 1cf644a..da69f5b 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -231,7 +231,48 @@
| 接口文档 | GET | `/docs` | FastAPI 自动生成 OpenAPI |
| 平面图静态 | GET | `/static/windoor-images/{id}.png` | 254 张采集平面图(开发参照,上线前须替换为自绘素材) |
-### 12.5 种子数据
+### 12.5 客户模块 `/api/customers`(v0.2 新增)
+
+| 接口名称 | 方式 | URL 路径 | 请求参数 | 说明 |
+|---|---|---|---|---|
+| 新增客户 | POST | `/api/customers` | `{name(必填), phone, address, note, salesmen}` | |
+| 客户列表 | GET | `/api/customers?page=1&page_size=15&keyword=` | query | 模糊匹配姓名/电话/地址 |
+| 编辑客户 | PUT | `/api/customers/{id}` | 同新增 | |
+| 删除客户 | DELETE | `/api/customers/{id}` | 路径 | |
+
+### 12.6 合同模块 `/api/contracts`(v0.2 新增)
+
+| 接口名称 | 方式 | URL 路径 | 请求参数 | 说明 |
+|---|---|---|---|---|
+| 创建合同 | POST | `/api/contracts` | `{customer_id, customer_name, phone, address, area, count, money, note}` | 合同号 `FG+YYYYMMDD+3位流水`,默认意向单 |
+| 合同列表 | GET | `/api/contracts?page=1&keyword=&status=` | query | 响应 `data.contracts` 分页(对齐目标站结构) |
+| 编辑合同 | PUT | `/api/contracts/{id}` | 同创建 | |
+| **意向单转正式** | POST | `/api/contracts/convert` | `{contract_id}` | 仅意向单可转,重复转换 code=0 |
+| 删除合同 | DELETE | `/api/contracts/{id}` | 路径 | |
+
+**合同→订单联动**:创建订单传 `contract_id` 时,后端自动快照合同的客户名/电话/地址到订单(客户资料后续变更不影响订单)。
+
+### 12.7 优化下料 `/api/optimize1d`(v0.2 新增,纯计算无状态)
+
+| 接口名称 | 方式 | URL 路径 | 请求参数 | 说明 |
+|---|---|---|---|---|
+| 一维优化 | POST | `/api/optimize1d` | `{demands:[{length,qty,label}], stocks:[{length,qty,label}], kerf:5}` | FFD 首递减装箱;返回 `plans[]`(每根原料的切割组合+余料)、`unplaced[]`、`stats.utilization`;需求>库存时 code=0 |
+
+### 12.8 前端(v0.2,Vue3 + Element Plus,构建产物挂载于后端)
+
+| 路由 | 页面 | 功能 |
+|---|---|---|
+| `/login` | 登录 | JWT 登录 |
+| `/dashboard` | 工作台 | 订单/意向单/客户/合同统计 + 快捷入口 |
+| `/orders` | 订单中心 | 搜索/新建(选合同+样板自动带图纸)/详情抽屉(**画布渲染 drawing_data**)+状态机操作+生产进度更新 |
+| `/contracts` | 合同管理 | 列表/新建(选客户带出信息)/意向单转正式/一键下订单 |
+| `/customers` | 客户管理 | CRUD + 搜索 |
+| `/templates` | 样板库 | 254 套样板网格(平面图) + 分类筛选 + 点击预览(**画布渲染**) + 用此样板下单 |
+| `/optimize` | 优化下料 | 需求/原料编辑 + FFD 计算结果(利用率/切割组合表) |
+
+**画布渲染器**(`components/WindoorCanvas.vue`,自研 Canvas2D):解析 drawing_data → Frame 玻璃区域多边形填充/描边 → 外框加粗 → 扇开向三角标记(od 角度) → 总宽高尺寸标注;支持滚轮缩放、拖拽平移、双击复位。
+
+### 12.9 种子数据
```bash
uv sync # 安装依赖
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..529a5af
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 华门窗云平台
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..f777e28
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1886 @@
+{
+ "name": "huamenchuang-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "huamenchuang-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.0",
+ "axios": "^1.7.0",
+ "element-plus": "^2.9.0",
+ "pinia": "^2.2.0",
+ "vue": "^3.5.0",
+ "vue-router": "^4.4.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.1.0",
+ "typescript": "^5.5.0",
+ "vite": "^5.4.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.9",
+ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.9.tgz",
+ "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz",
+ "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+ "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz",
+ "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz",
+ "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.8.0",
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz",
+ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.8",
+ "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+ "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz",
+ "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz",
+ "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz",
+ "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz",
+ "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz",
+ "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz",
+ "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz",
+ "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz",
+ "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz",
+ "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz",
+ "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz",
+ "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz",
+ "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz",
+ "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz",
+ "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz",
+ "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz",
+ "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz",
+ "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz",
+ "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz",
+ "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz",
+ "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz",
+ "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz",
+ "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz",
+ "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz",
+ "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz",
+ "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz",
+ "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.43.tgz",
+ "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/shared": "3.5.43",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz",
+ "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz",
+ "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/compiler-core": "3.5.43",
+ "@vue/compiler-dom": "3.5.43",
+ "@vue/compiler-ssr": "3.5.43",
+ "@vue/shared": "3.5.43",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.28",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz",
+ "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.43.tgz",
+ "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.43.tgz",
+ "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz",
+ "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.43",
+ "@vue/runtime-core": "3.5.43",
+ "@vue/shared": "3.5.43",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.43.tgz",
+ "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.43",
+ "@vue/runtime-dom": "3.5.43",
+ "@vue/shared": "3.5.43"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.43.tgz",
+ "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-15.0.0.tgz",
+ "integrity": "sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "15.0.0",
+ "@vueuse/shared": "15.0.0"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-15.0.0.tgz",
+ "integrity": "sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-15.0.0.tgz",
+ "integrity": "sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmmirror.com/axios/-/axios-1.20.0.tgz",
+ "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.6",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.23",
+ "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz",
+ "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/element-plus": {
+ "version": "2.14.6",
+ "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.6.tgz",
+ "integrity": "sha512-GH0fMoMgVEKkHimoaKvg5B00NmfPbQ+qxX6/JrO7F+dLAC3atrZr2Y2MoccHMxS22M7QE12/s0DG0Zs49iwJ/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.2.1",
+ "@element-plus/icons-vue": "^2.3.2",
+ "@floating-ui/dom": "^1.8.0",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
+ "@types/lodash": "^4.17.24",
+ "@types/lodash-es": "^4.17.12",
+ "@vueuse/core": "15.0.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.20",
+ "lodash": "^4.18.1",
+ "lodash-es": "^4.18.1",
+ "lodash-unified": "^1.0.3",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0",
+ "vue-component-type-helpers": "^3.3.11"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.7"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.19",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.19.tgz",
+ "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/pinia": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz",
+ "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.3",
+ "vue-demi": "^0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.4.4",
+ "vue": "^2.7.0 || ^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.4.tgz",
+ "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.63.4",
+ "@rollup/rollup-android-arm64": "4.63.4",
+ "@rollup/rollup-darwin-arm64": "4.63.4",
+ "@rollup/rollup-darwin-x64": "4.63.4",
+ "@rollup/rollup-freebsd-arm64": "4.63.4",
+ "@rollup/rollup-freebsd-x64": "4.63.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.63.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.63.4",
+ "@rollup/rollup-linux-arm64-musl": "4.63.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.63.4",
+ "@rollup/rollup-linux-loong64-musl": "4.63.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.63.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.63.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.63.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.63.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.63.4",
+ "@rollup/rollup-linux-x64-gnu": "4.63.4",
+ "@rollup/rollup-linux-x64-musl": "4.63.4",
+ "@rollup/rollup-openbsd-x64": "4.63.4",
+ "@rollup/rollup-openharmony-arm64": "4.63.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.63.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.63.4",
+ "@rollup/rollup-win32-x64-gnu": "4.63.4",
+ "@rollup/rollup-win32-x64-msvc": "4.63.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.43",
+ "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.43.tgz",
+ "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.43",
+ "@vue/compiler-sfc": "3.5.43",
+ "@vue/runtime-dom": "3.5.43",
+ "@vue/server-renderer": "3.5.43",
+ "@vue/shared": "3.5.43"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-component-type-helpers": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz",
+ "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==",
+ "license": "MIT"
+ },
+ "node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..f7cbf3c
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "huamenchuang-frontend",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.7.0",
+ "element-plus": "^2.9.0",
+ "@element-plus/icons-vue": "^2.3.0",
+ "pinia": "^2.2.0",
+ "vue": "^3.5.0",
+ "vue-router": "^4.4.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.1.0",
+ "typescript": "^5.5.0",
+ "vite": "^5.4.0"
+ }
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..99c5483
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
new file mode 100644
index 0000000..8477d6b
--- /dev/null
+++ b/frontend/src/api/index.ts
@@ -0,0 +1,43 @@
+/** axios 实例:自动携带 token、统一处理 401/付费墙 */
+import axios from 'axios'
+import { ElMessage } from 'element-plus'
+import router from '../router'
+
+export const http = axios.create({ baseURL: '/', timeout: 30000 })
+
+http.interceptors.request.use(cfg => {
+ const token = localStorage.getItem('hmc_token')
+ if (token) cfg.headers.Authorization = `Bearer ${token}`
+ return cfg
+})
+
+http.interceptors.response.use(
+ res => {
+ const d = res.data
+ if (d && typeof d === 'object' && 'code' in d) {
+ if (d.code === 1) return res
+ if (String(d.code) === '10086') {
+ ElMessage.warning(d.message ?? '该功能需要购买解锁')
+ return Promise.reject(new Error(d.message))
+ }
+ if (Number(d.code) === 401) {
+ localStorage.removeItem('hmc_token')
+ router.push('/login')
+ return Promise.reject(new Error(d.message))
+ }
+ ElMessage.error(d.message ?? '操作失败')
+ return Promise.reject(new Error(d.message))
+ }
+ return res
+ },
+ err => {
+ ElMessage.error(err.message ?? '网络错误')
+ return Promise.reject(err)
+ },
+)
+
+/** 提取 envelope 的 data */
+export async function api(method: string, url: string, body?: object): Promise {
+ const res = await http.request({ method, url, data: body })
+ return res.data.data as T
+}
diff --git a/frontend/src/components/WindoorCanvas.vue b/frontend/src/components/WindoorCanvas.vue
new file mode 100644
index 0000000..1aac177
--- /dev/null
+++ b/frontend/src/components/WindoorCanvas.vue
@@ -0,0 +1,232 @@
+
+
+
+
+ {{ info }}
+ 重置视图
+
+
+
+
+
+
+
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
new file mode 100644
index 0000000..d0271bf
--- /dev/null
+++ b/frontend/src/main.ts
@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import 'element-plus/dist/index.css'
+import App from './App.vue'
+import router from './router'
+
+createApp(App).use(createPinia()).use(router).use(ElementPlus, { locale: zhCn }).mount('#app')
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
new file mode 100644
index 0000000..bbd29c1
--- /dev/null
+++ b/frontend/src/router/index.ts
@@ -0,0 +1,31 @@
+/** 路由:登录守卫 + 主布局 */
+import { createRouter, createWebHistory } from 'vue-router'
+import { useAuthStore } from '../stores/auth'
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ { path: '/login', component: () => import('../views/LoginView.vue') },
+ {
+ path: '/',
+ component: () => import('../views/LayoutView.vue'),
+ children: [
+ { path: '', redirect: '/dashboard' },
+ { path: 'dashboard', component: () => import('../views/DashboardView.vue'), meta: { title: '工作台' } },
+ { path: 'orders', component: () => import('../views/OrdersView.vue'), meta: { title: '订单管理' } },
+ { path: 'contracts', component: () => import('../views/ContractsView.vue'), meta: { title: '合同管理' } },
+ { path: 'customers', component: () => import('../views/CustomersView.vue'), meta: { title: '客户管理' } },
+ { path: 'templates', component: () => import('../views/TemplatesView.vue'), meta: { title: '样板库' } },
+ { path: 'optimize', component: () => import('../views/OptimizeView.vue'), meta: { title: '优化下料' } },
+ ],
+ },
+ ],
+})
+
+router.beforeEach(to => {
+ const auth = useAuthStore()
+ if (to.path !== '/login' && !auth.isLogin) return '/login'
+ return true
+})
+
+export default router
diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts
new file mode 100644
index 0000000..fb62c61
--- /dev/null
+++ b/frontend/src/stores/auth.ts
@@ -0,0 +1,24 @@
+/** 认证状态 */
+import { defineStore } from 'pinia'
+
+export const useAuthStore = defineStore('auth', {
+ state: () => ({
+ token: localStorage.getItem('hmc_token') ?? '',
+ nickname: localStorage.getItem('hmc_nickname') ?? '',
+ }),
+ getters: { isLogin: s => !!s.token },
+ actions: {
+ setSession(token: string, nickname: string) {
+ this.token = token
+ this.nickname = nickname
+ localStorage.setItem('hmc_token', token)
+ localStorage.setItem('hmc_nickname', nickname)
+ },
+ logout() {
+ this.token = ''
+ this.nickname = ''
+ localStorage.removeItem('hmc_token')
+ localStorage.removeItem('hmc_nickname')
+ },
+ },
+})
diff --git a/frontend/src/types/drawing.ts b/frontend/src/types/drawing.ts
new file mode 100644
index 0000000..8eddf31
--- /dev/null
+++ b/frontend/src/types/drawing.ts
@@ -0,0 +1,106 @@
+/** drawing_data 数据模型类型(与采集的 shapeManager 序化格式同构) */
+
+export interface Pt { x: number; y: number; name?: string }
+export interface Vec { x: number; y: number; name?: string }
+export interface Segment { ps: Pt; pe: Pt; name?: string }
+
+/** 中梃分割线 */
+export interface SplitLine {
+ line: { pt: Pt; norm: Vec; name?: string }
+ polyId?: { idx: number; pos: number }
+ alignType?: number
+ equalSplit?: number
+ kw?: boolean // 竖撑
+ ws?: boolean // 窗撑
+ reinforced?: boolean
+ db?: boolean // hidden/disabled
+}
+
+/** 尺寸标注 */
+export interface Dim {
+ at: number; ds: boolean
+ ov?: Vec
+ eidx?: number // 绑定外框边
+ midx?: number // 绑定中梃
+ name?: string
+ type?: 'line' | 'mullion' | string
+}
+
+export interface FrameModel {
+ ew?: [number, number, number, number] // 四边料宽
+ vs?: Pt[]
+ db?: unknown[]
+ ejw?: number[]
+ pew?: unknown[]
+ ast?: unknown[]
+}
+
+export interface ColorMap {
+ g?: string // 玻璃填充
+ bn?: string // 边框
+ hd?: string // 高光
+ ben?: string
+}
+
+export interface SashShape {
+ type: 'Sash' | string
+ bd?: { from: Pt; to: Pt; hidden?: boolean } // 扇所在区域包围盒
+ od?: number // 开向角度
+ opened?: boolean
+ wtf?: boolean // 带转框
+ isDoor?: boolean
+ polyId?: { idx: number; pos: number }
+ enabled?: boolean
+ rid?: string
+ [k: string]: unknown
+}
+
+export interface FrameShape {
+ type: 'Frame' | string
+ polygon?: {
+ type?: string
+ polygon?: Segment[][] // 分割后的玻璃区域(每区域为闭合段列表)
+ spLine?: SplitLine
+ ep?: string[]
+ virtual?: boolean
+ }
+ fm?: FrameModel
+ mm?: { sp: SplitLine[]; dbs?: unknown[]; fls?: unknown[]; phs?: unknown[]; lpfs?: unknown[] }
+ dm?: { dims: Dim[] }
+ cm?: ColorMap
+ sm?: SashShape[] // 子形状(扇)
+ ps?: Record
+ uid?: string
+ titl?: string
+ [k: string]: unknown
+}
+
+export interface DrawingData {
+ s?: { cs?: number; cjs?: number }
+ cp?: unknown[]
+ sm: FrameShape[]
+ epi?: unknown[]
+ sbf?: unknown[]
+ wlc?: string
+ wall?: unknown[]
+ scale?: number
+ note?: unknown[]
+}
+
+/** 解析 drawing_data(兼容对象或 JSON 字符串) */
+export function parseDrawing(raw: string | object): DrawingData {
+ const dd = typeof raw === 'string' ? JSON.parse(raw) : raw
+ return dd as DrawingData
+}
+
+/** 区域多边形 → 点列(段首点顺序连接) */
+export function regionPoints(region: Segment[]): Pt[] {
+ return region.map(seg => ({ x: seg.ps.x, y: seg.ps.y }))
+}
+
+/** 点列包围盒 */
+export function bboxOf(points: Pt[]): { x0: number; y0: number; x1: number; y1: number } {
+ const xs = points.map(p => p.x)
+ const ys = points.map(p => p.y)
+ return { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) }
+}
diff --git a/frontend/src/views/ContractsView.vue b/frontend/src/views/ContractsView.vue
new file mode 100644
index 0000000..6419b34
--- /dev/null
+++ b/frontend/src/views/ContractsView.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.status_label }}
+
+
+
+
+
+ 转正式单
+ 下订单
+ 删
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 创建
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/CustomersView.vue b/frontend/src/views/CustomersView.vue
new file mode 100644
index 0000000..60386fa
--- /dev/null
+++ b/frontend/src/views/CustomersView.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 编辑
+ 删
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue
new file mode 100644
index 0000000..168fc14
--- /dev/null
+++ b/frontend/src/views/DashboardView.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+ {{ card.value }}{{ card.label }}
+
+
+
+
+ 快捷入口
+
+ 浏览样板库
+ 新建订单
+ 录入合同
+ 优化下料
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/LayoutView.vue b/frontend/src/views/LayoutView.vue
new file mode 100644
index 0000000..acb0cc9
--- /dev/null
+++ b/frontend/src/views/LayoutView.vue
@@ -0,0 +1,50 @@
+
+
+
+ 华门窗
+
+ 工作台
+ 订单管理
+ 合同管理
+ 客户管理
+ 样板库
+ 优化下料
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue
new file mode 100644
index 0000000..c1164d3
--- /dev/null
+++ b/frontend/src/views/LoginView.vue
@@ -0,0 +1,54 @@
+
+
+
+ 华门窗云平台
+ 画图 · 报价 · 订单 · 生产
+
+
+
+
+
+
+
+
+ 登 录
+
+
+ 开发账号:13800000000 / 123456
+
+
+
+
+
+
+
diff --git a/frontend/src/views/OptimizeView.vue b/frontend/src/views/OptimizeView.vue
new file mode 100644
index 0000000..0830a04
--- /dev/null
+++ b/frontend/src/views/OptimizeView.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ 删
+
+ + 添加需求
+
+
+
+
+
+
+
+
+ 删
+
+ + 添加原料
+
+
+
+
+
+ 锯口损耗:
+
+ 开始优化
+
+
+
+ {{ result.stats.total_pieces }}
+ {{ result.stats.stock_bars_used }}
+ {{ (result.stats.utilization * 100).toFixed(1) }}%
+ {{ result.unplaced.length }}
+
+
+
+ {{ row.stock_label }}({{ row.stock_length }}mm)
+
+
+
+ {{ c.label || '件' }} {{ c.length }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/OrdersView.vue b/frontend/src/views/OrdersView.vue
new file mode 100644
index 0000000..67bbcef
--- /dev/null
+++ b/frontend/src/views/OrdersView.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.status_label }}
+
+
+
+
+
+ 图纸
+ {{ act.label }}
+ 删
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 创建订单
+
+
+
+
+
+
+
+ {{ detail.status_label }}
+ 客户:{{ detail.customer_name || '-' }}
+ 金额:{{ detail.money }}
+ 面积:{{ detail.area }}㎡
+
+
+
+
+
生产进度
+
+
+
+
+
+
+
+
+
+
+ 更新进度
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/TemplatesView.vue b/frontend/src/views/TemplatesView.vue
new file mode 100644
index 0000000..59be384
--- /dev/null
+++ b/frontend/src/views/TemplatesView.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
{{ t.name || `样板#${t.id}` }}
+
+
+
+
+
+
+
+
+
+
+
+ 尺寸限制: {{ current.low_width }}-{{ current.high_width }} × {{ current.low_height }}-{{ current.high_height }} mm
+ 引用次数: {{ current.times }}
+ 用此样板下单
+
+
+
+
+
+
+
+
+
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..aa5f143
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "jsx": "preserve",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "types": ["vite/client"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.vue"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..cd6100d
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// 开发时 /api 代理到后端 8000;构建产物输出 dist 由后端挂载
+export default defineConfig({
+ plugins: [vue()],
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': { target: 'http://localhost:8000', changeOrigin: true },
+ '/static': { target: 'http://localhost:8000', changeOrigin: true },
+ },
+ },
+ build: { outDir: 'dist', emptyOutDir: true },
+})
diff --git a/src/huamenchuang/api/contracts.py b/src/huamenchuang/api/contracts.py
new file mode 100644
index 0000000..0c81485
--- /dev/null
+++ b/src/huamenchuang/api/contracts.py
@@ -0,0 +1,85 @@
+"""合同路由:意向单/正式单管理。"""
+from fastapi import APIRouter
+from pydantic import BaseModel, Field
+
+from huamenchuang.core.deps import CurrentUser, DbSession
+from huamenchuang.core.response import fail, ok, paginated
+from huamenchuang.repositories.contract_repo import status_label
+from huamenchuang.services.contract_service import ContractService
+
+router = APIRouter(prefix="/contracts", tags=["合同"])
+
+
+class ContractBody(BaseModel):
+ customer_id: int | None = None
+ customer_name: str = ""
+ phone: str = ""
+ address: str = ""
+ area: float = 0
+ count: int = 0
+ money: float = 0
+ note: str = ""
+
+
+class ConvertBody(BaseModel):
+ contract_id: int
+
+
+def _to_out(c) -> dict:
+ return {
+ "id": c.id, "contract_no": c.contract_no, "customer_id": c.customer_id,
+ "customer_name": c.customer_name, "phone": c.phone, "address": c.address,
+ "area": float(c.area), "count": c.count, "money": float(c.money),
+ "status": c.status, "status_label": status_label(c.status),
+ "sign_status": c.sign_status, "note": c.note,
+ "created_at": str(c.created_at), "updated_at": str(c.updated_at),
+ }
+
+
+@router.post("")
+async def create_contract(body: ContractBody, user: CurrentUser, db: DbSession):
+ """创建合同(默认意向单)。"""
+ contract = await ContractService(db).create(user, body.model_dump())
+ return ok(data=_to_out(contract), message="创建成功")
+
+
+@router.get("")
+async def list_contracts(user: CurrentUser, db: DbSession, page: int = 1,
+ page_size: int = 15, keyword: str = "", status: int | None = None):
+ """合同分页列表(结构对齐目标站 {"contracts": {...}})。"""
+ items, total = await ContractService(db).repo.search(
+ user.company_id, page, page_size, keyword, status)
+ return ok(data={"contracts": paginated(
+ [_to_out(c) for c in items], page, page_size, total)})
+
+
+@router.put("/{contract_id}")
+async def update_contract(contract_id: int, body: ContractBody, user: CurrentUser, db: DbSession):
+ """编辑合同。"""
+ try:
+ c = await ContractService(db).update(user, contract_id, body.model_dump())
+ except KeyError as e:
+ return fail(str(e))
+ return ok(data=_to_out(c), message="更新成功")
+
+
+@router.post("/convert")
+async def convert_contract(body: ConvertBody, user: CurrentUser, db: DbSession):
+ """意向单转正式单(对齐目标站 convert-contract/)。"""
+ try:
+ c = await ContractService(db).convert_to_formal(user, body.contract_id)
+ except KeyError as e:
+ return fail(str(e))
+ except ValueError as e:
+ return fail(str(e))
+ return ok(data=_to_out(c), message="已转为正式单")
+
+
+@router.delete("/{contract_id}")
+async def delete_contract(contract_id: int, user: CurrentUser, db: DbSession):
+ """删除合同。"""
+ try:
+ await ContractService(db).delete(user, contract_id)
+ except KeyError as e:
+ return fail(str(e))
+ return ok(message="删除成功")
diff --git a/src/huamenchuang/api/customers.py b/src/huamenchuang/api/customers.py
new file mode 100644
index 0000000..a8b834c
--- /dev/null
+++ b/src/huamenchuang/api/customers.py
@@ -0,0 +1,68 @@
+"""客户路由。"""
+from fastapi import APIRouter
+from pydantic import BaseModel, Field
+
+from huamenchuang.core.deps import CurrentUser, DbSession
+from huamenchuang.core.response import fail, ok, paginated
+from huamenchuang.services.customer_service import CustomerService
+
+router = APIRouter(prefix="/customers", tags=["客户"])
+
+
+class CustomerBody(BaseModel):
+ name: str = Field(min_length=1, max_length=128)
+ phone: str = ""
+ address: str = ""
+ note: str = ""
+ salesmen: str | None = None
+
+
+@router.post("")
+async def create_customer(body: CustomerBody, user: CurrentUser, db: DbSession):
+ """新增客户。"""
+ customer = await CustomerService(db).create(user, body.model_dump())
+ return ok(data={"id": customer.id, "name": customer.name}, message="创建成功")
+
+
+@router.post("/search")
+async def search_customers(user: CurrentUser, db: DbSession,
+ page: int = 1, page_size: int = 15, keyword: str = ""):
+ """客户搜索。"""
+ items, total = await CustomerService(db).repo.search(user.company_id, page, page_size, keyword)
+ return ok(data=paginated([{
+ "id": c.id, "name": c.name, "phone": c.phone, "address": c.address,
+ "note": c.note, "salesmen": c.salesmen, "balance": float(c.balance),
+ "created_at": str(c.created_at),
+ } for c in items], page, page_size, total))
+
+
+@router.get("")
+async def list_customers(user: CurrentUser, db: DbSession, page: int = 1,
+ page_size: int = 15, keyword: str = ""):
+ """客户分页列表。"""
+ items, total = await CustomerService(db).repo.search(user.company_id, page, page_size, keyword)
+ return ok(data=paginated([{
+ "id": c.id, "name": c.name, "phone": c.phone, "address": c.address,
+ "note": c.note, "salesmen": c.salesmen, "balance": float(c.balance),
+ "created_at": str(c.created_at),
+ } for c in items], page, page_size, total))
+
+
+@router.put("/{customer_id}")
+async def update_customer(customer_id: int, body: CustomerBody, user: CurrentUser, db: DbSession):
+ """编辑客户。"""
+ try:
+ c = await CustomerService(db).update(user, customer_id, body.model_dump())
+ except KeyError as e:
+ return fail(str(e))
+ return ok(data={"id": c.id}, message="更新成功")
+
+
+@router.delete("/{customer_id}")
+async def delete_customer(customer_id: int, user: CurrentUser, db: DbSession):
+ """删除客户。"""
+ try:
+ await CustomerService(db).delete(user, customer_id)
+ except KeyError as e:
+ return fail(str(e))
+ return ok(message="删除成功")
diff --git a/src/huamenchuang/api/optimize.py b/src/huamenchuang/api/optimize.py
new file mode 100644
index 0000000..433a5c0
--- /dev/null
+++ b/src/huamenchuang/api/optimize.py
@@ -0,0 +1,28 @@
+"""优化下料路由:一维型材切割方案计算(无状态,纯计算)。"""
+from fastapi import APIRouter
+from pydantic import BaseModel, Field
+
+from huamenchuang.core.deps import CurrentUser
+from huamenchuang.core.response import fail, ok
+from huamenchuang.services.optimize_service import BarDemand, StockBar, optimize_1d
+
+router = APIRouter(prefix="/optimize1d", tags=["优化下料"])
+
+
+class OptimizeRequest(BaseModel):
+ demands: list[BarDemand] = Field(description="型材需求 [{length,qty,label}]")
+ stocks: list[StockBar] = Field(description="库存原料 [{length,qty,label}]")
+ kerf: int = Field(default=5, ge=0, le=50, description="锯口损耗(mm)")
+
+
+@router.post("")
+async def optimize(body: OptimizeRequest, user: CurrentUser):
+ """一维优化下料(FFD 首递减算法)。"""
+ if not body.demands or not body.stocks:
+ return fail("需求和库存均不能为空")
+ total_demand = sum(d.length * d.qty for d in body.demands)
+ total_stock = sum(s.length * s.qty for s in body.stocks)
+ if total_demand > total_stock:
+ return fail(f"库存不足: 需求 {total_demand}mm > 库存 {total_stock}mm")
+ result = optimize_1d(body.demands, body.stocks, body.kerf)
+ return ok(data=result, message="优化完成")
diff --git a/src/huamenchuang/api/router.py b/src/huamenchuang/api/router.py
index f1906ad..9ccf529 100644
--- a/src/huamenchuang/api/router.py
+++ b/src/huamenchuang/api/router.py
@@ -1,9 +1,12 @@
"""路由汇总。"""
from fastapi import APIRouter
-from huamenchuang.api import auth, orders, windoor
+from huamenchuang.api import auth, contracts, customers, optimize, orders, windoor
api_router = APIRouter(prefix="/api")
api_router.include_router(auth.router)
api_router.include_router(orders.router)
+api_router.include_router(customers.router)
+api_router.include_router(contracts.router)
api_router.include_router(windoor.router)
+api_router.include_router(optimize.router)
diff --git a/src/huamenchuang/main.py b/src/huamenchuang/main.py
index 5876ec8..6904147 100644
--- a/src/huamenchuang/main.py
+++ b/src/huamenchuang/main.py
@@ -53,15 +53,28 @@ async def health():
app.include_router(api_router)
+from fastapi.responses import FileResponse # noqa: E402
+
# 静态资源:样板平面图(data/windoor-images)
_imgdir = Path(__file__).resolve().parents[2] / "data" / "windoor-images"
if _imgdir.exists():
app.mount("/static/windoor-images", StaticFiles(directory=str(_imgdir)), name="windoor-images")
-# 前端构建产物挂载(前后端规范:构建到后端目录)
+# 前端构建产物挂载(前后端规范:构建到后端目录)+ history 路由回退
_dist = Path(__file__).resolve().parents[2] / "frontend" / "dist"
if _dist.exists():
- app.mount("/", StaticFiles(directory=str(_dist), html=True), name="frontend")
+ app.mount("/assets", StaticFiles(directory=str(_dist / "assets")), name="assets")
+
+ @app.get("/{full_path:path}", include_in_schema=False)
+ async def spa_fallback(full_path: str):
+ """非 API/静态路径一律返回 index.html(前端 history 模式)。"""
+ if full_path.startswith(("api/", "static/")):
+ return JSONResponse(
+ {"code": 0, "message": "接口不存在", "data": "", "other_data": "", "other_data2": ""})
+ target = _dist / full_path
+ if full_path and target.is_file():
+ return FileResponse(target)
+ return FileResponse(_dist / "index.html")
def main() -> None:
diff --git a/src/huamenchuang/repositories/contract_repo.py b/src/huamenchuang/repositories/contract_repo.py
new file mode 100644
index 0000000..2dccda8
--- /dev/null
+++ b/src/huamenchuang/repositories/contract_repo.py
@@ -0,0 +1,55 @@
+"""合同数据访问层。"""
+import datetime
+
+from sqlalchemy import func, or_, select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from huamenchuang.models.contract import Contract
+
+STATUS_LABELS = {0: "意向单", 1: "正式单", 2: "已完成", 3: "已作废"}
+
+
+class ContractRepository:
+ def __init__(self, db: AsyncSession) -> None:
+ self.db = db
+
+ async def next_contract_no(self, company_id: int) -> str:
+ """合同号:FG+YYYYMMDD+3位流水(对齐目标站 FG20260818009 风格)。"""
+ today = datetime.date.today().strftime("%Y%m%d")
+ prefix = f"FG{today}"
+ stmt = select(Contract.contract_no).where(
+ Contract.company_id == company_id, Contract.contract_no.like(f"{prefix}%")
+ ).order_by(Contract.contract_no.desc()).limit(1)
+ last = (await self.db.execute(stmt)).scalar_one_or_none()
+ seq = int(last[-3:]) + 1 if last else 1
+ return f"{prefix}{seq:03d}"
+
+ async def get(self, contract_id: int, company_id: int) -> Contract | None:
+ stmt = select(Contract).where(Contract.id == contract_id, Contract.company_id == company_id)
+ return (await self.db.execute(stmt)).scalar_one_or_none()
+
+ async def search(self, company_id: int, page: int, page_size: int, keyword: str = "",
+ status: int | None = None) -> tuple[list[Contract], int]:
+ conds = [Contract.company_id == company_id]
+ if status is not None:
+ conds.append(Contract.status == status)
+ if kw := keyword.strip():
+ like = f"%{kw}%"
+ conds.append(or_(Contract.contract_no.like(like), Contract.customer_name.like(like), Contract.phone.like(like)))
+ where = *conds,
+ total = (await self.db.execute(select(func.count()).select_from(Contract).where(*where))).scalar_one()
+ stmt = (select(Contract).where(*where).order_by(Contract.id.desc())
+ .offset((page - 1) * page_size).limit(page_size))
+ return list((await self.db.execute(stmt)).scalars()), total
+
+ async def create(self, contract: Contract) -> Contract:
+ self.db.add(contract)
+ await self.db.flush()
+ return contract
+
+ async def delete(self, contract: Contract) -> None:
+ await self.db.delete(contract)
+
+
+def status_label(status: int) -> str:
+ return STATUS_LABELS.get(status, "未知")
diff --git a/src/huamenchuang/repositories/customer_repo.py b/src/huamenchuang/repositories/customer_repo.py
new file mode 100644
index 0000000..c7b1f91
--- /dev/null
+++ b/src/huamenchuang/repositories/customer_repo.py
@@ -0,0 +1,33 @@
+"""客户数据访问层。"""
+from sqlalchemy import func, or_, select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from huamenchuang.models.contract import Customer
+
+
+class CustomerRepository:
+ def __init__(self, db: AsyncSession) -> None:
+ self.db = db
+
+ async def get(self, customer_id: int, company_id: int) -> Customer | None:
+ stmt = select(Customer).where(Customer.id == customer_id, Customer.company_id == company_id)
+ return (await self.db.execute(stmt)).scalar_one_or_none()
+
+ async def search(self, company_id: int, page: int, page_size: int, keyword: str = "") -> tuple[list[Customer], int]:
+ conds = [Customer.company_id == company_id]
+ if kw := keyword.strip():
+ like = f"%{kw}%"
+ conds.append(or_(Customer.name.like(like), Customer.phone.like(like), Customer.address.like(like)))
+ where = *conds,
+ total = (await self.db.execute(select(func.count()).select_from(Customer).where(*where))).scalar_one()
+ stmt = (select(Customer).where(*where).order_by(Customer.id.desc())
+ .offset((page - 1) * page_size).limit(page_size))
+ return list((await self.db.execute(stmt)).scalars()), total
+
+ async def create(self, customer: Customer) -> Customer:
+ self.db.add(customer)
+ await self.db.flush()
+ return customer
+
+ async def delete(self, customer: Customer) -> None:
+ await self.db.delete(customer)
diff --git a/src/huamenchuang/services/contract_service.py b/src/huamenchuang/services/contract_service.py
new file mode 100644
index 0000000..fac29b9
--- /dev/null
+++ b/src/huamenchuang/services/contract_service.py
@@ -0,0 +1,44 @@
+"""合同业务逻辑层:意向单→正式单流转。"""
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from huamenchuang.models.contract import Contract
+from huamenchuang.models.user import User
+from huamenchuang.repositories.contract_repo import ContractRepository
+
+
+class ContractService:
+ def __init__(self, db: AsyncSession) -> None:
+ self.repo = ContractRepository(db)
+
+ async def create(self, user: User, data: dict) -> Contract:
+ contract = await self.repo.create(Contract(
+ company_id=user.company_id,
+ user_id=user.id,
+ contract_no=await self.repo.next_contract_no(user.company_id),
+ **data,
+ ))
+ return contract
+
+ async def update(self, user: User, contract_id: int, data: dict) -> Contract:
+ contract = await self.repo.get(contract_id, user.company_id)
+ if contract is None:
+ raise KeyError("合同不存在")
+ for k, v in data.items():
+ setattr(contract, k, v)
+ return contract
+
+ async def convert_to_formal(self, user: User, contract_id: int) -> Contract:
+ """意向单转正式单(对齐目标站 convert-contract 语义)。"""
+ contract = await self.repo.get(contract_id, user.company_id)
+ if contract is None:
+ raise KeyError("合同不存在")
+ if contract.status != 0:
+ raise ValueError("仅意向单可转正式单")
+ contract.status = 1
+ return contract
+
+ async def delete(self, user: User, contract_id: int) -> None:
+ contract = await self.repo.get(contract_id, user.company_id)
+ if contract is None:
+ raise KeyError("合同不存在")
+ await self.repo.delete(contract)
diff --git a/src/huamenchuang/services/customer_service.py b/src/huamenchuang/services/customer_service.py
new file mode 100644
index 0000000..4a83fcc
--- /dev/null
+++ b/src/huamenchuang/services/customer_service.py
@@ -0,0 +1,28 @@
+"""客户业务逻辑层。"""
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from huamenchuang.models.contract import Customer
+from huamenchuang.models.user import User
+from huamenchuang.repositories.customer_repo import CustomerRepository
+
+
+class CustomerService:
+ def __init__(self, db: AsyncSession) -> None:
+ self.repo = CustomerRepository(db)
+
+ async def create(self, user: User, data: dict) -> Customer:
+ return await self.repo.create(Customer(company_id=user.company_id, **data))
+
+ async def update(self, user: User, customer_id: int, data: dict) -> Customer:
+ customer = await self.repo.get(customer_id, user.company_id)
+ if customer is None:
+ raise KeyError("客户不存在")
+ for k, v in data.items():
+ setattr(customer, k, v)
+ return customer
+
+ async def delete(self, user: User, customer_id: int) -> None:
+ customer = await self.repo.get(customer_id, user.company_id)
+ if customer is None:
+ raise KeyError("客户不存在")
+ await self.repo.delete(customer)
diff --git a/src/huamenchuang/services/optimize_service.py b/src/huamenchuang/services/optimize_service.py
new file mode 100644
index 0000000..c5ca609
--- /dev/null
+++ b/src/huamenchuang/services/optimize_service.py
@@ -0,0 +1,104 @@
+"""一维优化下料服务:首递减(FFD)装箱算法,输出切割方案与利用率。
+
+对齐目标站 optimize1d 语义:输入订单型材需求 + 库存原料,输出每根原料的切割组合。
+"""
+from dataclasses import dataclass, field
+
+
+@dataclass
+class BarDemand:
+ """一段型材需求。"""
+ length: int # 需求长度(mm)
+ qty: int # 数量
+ label: str = "" # 用途标签
+
+
+@dataclass
+class StockBar:
+ """库存原料。"""
+ length: int # 原料长度(mm)
+ qty: int # 库存根数
+ label: str = ""
+
+
+@dataclass
+class CutPiece:
+ length: int
+ label: str = ""
+
+
+@dataclass
+class CutPlan:
+ """一根原料的切割方案。"""
+ stock_length: int
+ stock_label: str
+ pieces: list[CutPiece] = field(default_factory=list)
+ waste: int = 0 # 余料浪费
+
+
+# 锯口损耗(mm),每次切割损耗一段
+KERF_DEFAULT = 5
+
+
+def optimize_1d(demands: list[BarDemand], stocks: list[StockBar],
+ kerf: int = KERF_DEFAULT) -> dict:
+ """首递减装箱:长件优先装入可容纳的原料,输出方案与统计。"""
+ # 展开需求并降序
+ pieces: list[CutPiece] = []
+ for d in demands:
+ pieces.extend(CutPiece(length=d.length, label=d.label) for _ in range(max(0, d.qty)))
+ pieces.sort(key=lambda p: p.length, reverse=True)
+
+ # 展开库存(可切割长度 = 原料长)
+ avail: list[dict] = []
+ for s in stocks:
+ for _ in range(max(0, s.qty)):
+ avail.append({"length": s.length, "label": s.label, "pieces": [], "used": 0})
+
+ unplaced: list[CutPiece] = []
+ for piece in pieces:
+ # 找第一根装得下的原料(剩余空间 >= 件长 + 锯口)
+ placed = False
+ for bar in avail:
+ remain = bar["length"] - bar["used"]
+ cost = piece.length + (kerf if bar["pieces"] else 0)
+ if remain >= cost:
+ bar["pieces"].append(piece)
+ bar["used"] += cost
+ placed = True
+ break
+ if not placed:
+ unplaced.append(piece)
+
+ plans = [
+ CutPlan(
+ stock_length=b["length"],
+ stock_label=b["label"],
+ pieces=b["pieces"],
+ waste=b["length"] - b["used"],
+ )
+ for b in avail if b["pieces"]
+ ]
+ total_demand = sum(p.length for p in pieces)
+ total_stock = sum(p.stock_length for p in plans)
+ return {
+ "plans": [
+ {
+ "stock_length": p.stock_length,
+ "stock_label": p.stock_label,
+ "pieces": [{"length": c.length, "label": c.label} for c in p.pieces],
+ "waste": p.waste,
+ }
+ for p in plans
+ ],
+ "unplaced": [{"length": c.length, "label": c.label} for c in unplaced],
+ "stats": {
+ "total_pieces": len(pieces),
+ "placed_pieces": len(pieces) - len(unplaced),
+ "stock_bars_used": len(plans),
+ "total_demand_mm": total_demand,
+ "total_stock_mm": total_stock,
+ "utilization": round(total_demand / total_stock, 4) if total_stock else 0,
+ "kerf_mm": kerf,
+ },
+ }
diff --git a/src/huamenchuang/services/order_service.py b/src/huamenchuang/services/order_service.py
index c8d17c6..fc8ae02 100644
--- a/src/huamenchuang/services/order_service.py
+++ b/src/huamenchuang/services/order_service.py
@@ -14,13 +14,26 @@ class OrderService:
self.repo = OrderRepository(db)
async def create_order(self, user: User, payload: OrderCreate) -> Order:
- """创建订单:对齐目标站 {contract_id, drawing_data, pic_url} 契约。"""
+ """创建订单:对齐目标站 {contract_id, drawing_data, pic_url} 契约。
+ 指定 contract_id 时自动快照合同客户信息。"""
+ customer_id = payload.customer_id
+ customer_name = phone = address = ""
+ if payload.contract_id:
+ from huamenchuang.repositories.contract_repo import ContractRepository
+ contract = await ContractRepository(self.db).get(payload.contract_id, user.company_id)
+ if contract is None:
+ raise KeyError("关联合同不存在")
+ customer_id = contract.customer_id
+ customer_name, phone, address = contract.customer_name, contract.phone, contract.address
order = Order(
order_no=await self.repo.next_order_no(user.company_id),
contract_id=payload.contract_id,
drawing_data=payload.drawing_data,
pic_url=payload.pic_url,
- customer_id=payload.customer_id,
+ customer_id=customer_id,
+ customer_name=customer_name,
+ phone=phone,
+ address=address,
note=payload.note,
money=payload.money,
count=payload.count,