diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..f363412
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,80 @@
+## 1. 项目背景与战略目标
+在大型项目(如字节跳动内部项目)中,测试用例的管理需要兼顾**灵活性**与**规范性**。本项目致力于实现:
+- **测试管理与创建**:支持多场景(Web、移动端、API等)测试用例的创建与维护,确保全链路覆盖。
+- **场景规范化**:通过模板化和标准化流程,统一不同研发、测试人员对业务场景的定义,减少沟通误差。
+- **报告数据化**:全量采集执行结果,自动生成多维报表,驱动效率优化与质量分析。
+
+## 2. 核心功能设计
+
+### 2.1 脑图与表格双向联动
+- **思维导图视图**:用于梳理测试逻辑、功能模块和边界情况。支持快捷键(Enter 新增兄弟节点,Tab 新增子节点)。
+- **表格视图**:用于录入详细的“操作步骤”、“预期结果”和“优先级”。
+- **实时同步**:在脑图中修改节点名称,表格自动更新;在表格中增删行,脑图结构随之变化。
+
+### 2.2 测试计划管理 (Test Plan)
+- **多类型支持**:支持创建不同类型的测试计划,包括**需求测试**、**回归测试**、**开发自测**、**冒烟测试**等。
+- **独立上下文**:每个测试计划可以关联不同的用例集,记录独立的执行状态和评审进度。
+- **一键拉群关联**:飞书拉群功能将直接关联到特定的测试计划,群内实时推送该计划的执行进度。
+
+### 2.3 用例详情与属性 (Property Panel)
+- **步骤管理**:右侧面板支持录入详细的“操作步骤”与“预期结果”,支持动态增删。
+- **优先级标记**:支持 P0-P3 快速切换。
+- **评审记录**:支持记录节点的状态变化。
+
+### 2.4 用例分级 (Priority Management)
+- **等级定义**:
+ - **P0**:冒烟测试核心用例,阻塞性故障。
+ - **P1**:主要功能路径。
+ - **P2**:次要功能及边界。
+ - **P3**:视觉、文案等细微问题。
+- **视觉呈现**:脑图节点支持标记 P0-P3 图标,表格支持按优先级过滤。
+
+### 2.3 飞书(Feishu/Lark)一键拉群
+- **场景**:针对某个“测试计划”或“高优用例集”,点击按钮直接调起飞书 API 创建群聊。
+- **功能点**:
+ - 自动邀请相关测试人员与开发人员。
+ - 群名自动生成(例如:`【测试专项】XX项目_20240428`)。
+ - 群内自动推送测试报告概览卡片。
+
+### 2.4 用例评审与执行
+- **评审流**:支持节点标记“已通过”、“待修改”。
+- **执行模式**:进入执行态后,点击节点可快速标记 Pass/Fail,并自动记录执行人。
+
+## 3. 技术架构方案
+
+### 3.1 前端技术栈
+- **框架**:React + TypeScript (提供强类型支持,适配复杂逻辑)。
+- **样式**:Vanilla CSS + CSS Variables (打造极致的响应式与动态主题,如深色模式)。
+- **脑图引擎**:基于 `React Flow` 或自研 SVG 引擎,实现平滑的缩放与拖拽。
+- **状态管理**:`Zustand` (轻量级,适配频繁同步的脑图数据)。
+
+### 3.2 数据结构设计
+用例以**树状结构**存储,每个节点包含:
+```json
+{
+ "id": "uuid",
+ "text": "登录功能",
+ "priority": "P0",
+ "children": [...],
+ "steps": [
+ {"action": "输入账号", "expected": "账号回显正确"}
+ ],
+ "status": "pass" | "fail" | "blocked"
+}
+```
+
+## 4. 界面交互设计 (Premium Aesthetics)
+- **配色**:采用字节跳动风格的“极客蓝”配合深色/亮色毛玻璃效果。
+- **动画**:使用 `Framer Motion` 实现视图切换时的平滑过渡。
+- **布局**:
+ - 左侧:项目/目录树。
+ - 顶部:工具栏(视图切换、飞书拉群、全局搜索)。
+ - 中央:核心编辑器(脑图/表格)。
+ - 右侧:属性面板(选中节点的详细信息、评审记录)。
+
+## 5. 开发路线图
+1. **Phase 1**: 基础框架搭建,实现脑图与表格的渲染。
+2. **Phase 2**: 实现数据的本地/持久化存储及双向同步逻辑。
+3. **Phase 3**: 集成用例标级功能与右侧属性面板。
+4. **Phase 4**: 模拟飞书 API 集成与一键拉群交互。
+5. **Phase 5**: 整体视觉打磨与性能优化。
diff --git a/backend/__pycache__/database.cpython-313.pyc b/backend/__pycache__/database.cpython-313.pyc
new file mode 100644
index 0000000..8e67e17
Binary files /dev/null and b/backend/__pycache__/database.cpython-313.pyc differ
diff --git a/backend/__pycache__/feishu_client.cpython-313.pyc b/backend/__pycache__/feishu_client.cpython-313.pyc
new file mode 100644
index 0000000..c9887fd
Binary files /dev/null and b/backend/__pycache__/feishu_client.cpython-313.pyc differ
diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc
new file mode 100644
index 0000000..3ebbf90
Binary files /dev/null and b/backend/__pycache__/main.cpython-313.pyc differ
diff --git a/backend/__pycache__/models.cpython-313.pyc b/backend/__pycache__/models.cpython-313.pyc
new file mode 100644
index 0000000..a36176f
Binary files /dev/null and b/backend/__pycache__/models.cpython-313.pyc differ
diff --git a/backend/add_column_migration.py b/backend/add_column_migration.py
new file mode 100644
index 0000000..b5c7227
--- /dev/null
+++ b/backend/add_column_migration.py
@@ -0,0 +1,40 @@
+import pymysql
+import os
+
+DB_HOST = "mysql1.rdsmbk3ednsgnnt.rds.bj.baidubce.com"
+DB_PORT = 3306
+DB_NAME = "case_platform"
+DB_USER = "root_dev"
+DB_PASS = "Kdse89sd"
+
+def run_migration():
+ try:
+ connection = pymysql.connect(
+ host=DB_HOST,
+ port=DB_PORT,
+ user=DB_USER,
+ password=DB_PASS,
+ database=DB_NAME,
+ charset='utf8mb4',
+ cursorclass=pymysql.cursors.DictCursor
+ )
+ with connection.cursor() as cursor:
+ # Check if assignees column exists in test_plans
+ cursor.execute("SHOW COLUMNS FROM test_plans LIKE 'assignees'")
+ result = cursor.fetchone()
+ if not result:
+ print("Adding 'assignees' column to 'test_plans' table...")
+ cursor.execute("ALTER TABLE test_plans ADD COLUMN assignees JSON NULL AFTER case_ids")
+ print("Column added successfully.")
+ else:
+ print("Column 'assignees' already exists in 'test_plans'.")
+
+ connection.commit()
+ except Exception as e:
+ print(f"Migration failed: {e}")
+ finally:
+ if 'connection' in locals():
+ connection.close()
+
+if __name__ == "__main__":
+ run_migration()
diff --git a/backend/add_column_migration_tasks.py b/backend/add_column_migration_tasks.py
new file mode 100644
index 0000000..a2ea2a8
--- /dev/null
+++ b/backend/add_column_migration_tasks.py
@@ -0,0 +1,40 @@
+import pymysql
+import os
+
+DB_HOST = "mysql1.rdsmbk3ednsgnnt.rds.bj.baidubce.com"
+DB_PORT = 3306
+DB_NAME = "case_platform"
+DB_USER = "root_dev"
+DB_PASS = "Kdse89sd"
+
+def run_migration():
+ try:
+ connection = pymysql.connect(
+ host=DB_HOST,
+ port=DB_PORT,
+ user=DB_USER,
+ password=DB_PASS,
+ database=DB_NAME,
+ charset='utf8mb4',
+ cursorclass=pymysql.cursors.DictCursor
+ )
+ with connection.cursor() as cursor:
+ # Check if assignees column exists in test_tasks
+ cursor.execute("SHOW COLUMNS FROM test_tasks LIKE 'assignees'")
+ result = cursor.fetchone()
+ if not result:
+ print("Adding 'assignees' column to 'test_tasks' table...")
+ cursor.execute("ALTER TABLE test_tasks ADD COLUMN assignees JSON NULL AFTER plan_id")
+ print("Column added successfully.")
+ else:
+ print("Column 'assignees' already exists in 'test_tasks'.")
+
+ connection.commit()
+ except Exception as e:
+ print(f"Migration failed: {e}")
+ finally:
+ if 'connection' in locals():
+ connection.close()
+
+if __name__ == "__main__":
+ run_migration()
diff --git a/backend/database.py b/backend/database.py
new file mode 100644
index 0000000..0d78c6e
--- /dev/null
+++ b/backend/database.py
@@ -0,0 +1,54 @@
+import os
+import pymysql
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.exc import OperationalError
+
+# ── 数据库配置 ──────────────────────────────────────────────────────────────
+DB_HOST = os.getenv("DB_HOST", "mysql1.rdsmbk3ednsgnnt.rds.bj.baidubce.com")
+DB_PORT = os.getenv("DB_PORT", "3306")
+DB_NAME = os.getenv("DB_NAME", "case_platform")
+DB_USER = os.getenv("DB_USER", "root_dev")
+DB_PASS = os.getenv("DB_PASS", "Kdse89sd")
+
+MYSQL_URL = f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
+SQLITE_URL = "sqlite:///./quantum_test.db"
+
+def get_engine():
+ try:
+ # 尝试连接 MySQL
+ engine = create_engine(
+ MYSQL_URL,
+ pool_pre_ping=True,
+ pool_recycle=1800,
+ pool_size=10,
+ max_overflow=20,
+ )
+ # 测试连接
+ with engine.connect() as conn:
+ pass
+ print("✅ 成功连接到 MySQL (百度云 RDS)")
+ return engine
+ except Exception as e:
+ print(f"⚠️ MySQL 连接失败 (IP白名单限制或凭证错误) - {e}")
+ print("🔄 自动降级:使用本地 SQLite 数据库以保证应用正常运行...")
+ engine = create_engine(
+ SQLITE_URL,
+ connect_args={"check_same_thread": False}
+ )
+ return engine
+
+engine = get_engine()
+
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+Base = declarative_base()
+
+
+def get_db():
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
diff --git a/backend/dump_mysql.sql b/backend/dump_mysql.sql
new file mode 100644
index 0000000..f9c54c4
--- /dev/null
+++ b/backend/dump_mysql.sql
@@ -0,0 +1,87 @@
+-- QuantumTest SQLite → MySQL 导出
+-- 生成时间: 2026-05-06 12:07:40
+-- 执行前请确保 case_platform 数据库已存在
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ─── test_cases (60 条) ───────────────
+DELETE FROM `test_cases`;
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-001', 'TC-001', '账号密码正确登录成功', '用户管理', 'General', 'P2', 'Reviewed', 'UNTESTED', '张三', 'REQ-1001', NULL, '[{"action": "\u6253\u5f00\u767b\u5f55\u9875\u9762", "expected": "\u767b\u5f55\u9875\u6b63\u5e38\u5c55\u793a"}, {"action": "\u8f93\u5165\u6b63\u786e\u7684\u8d26\u53f7\u5bc6\u7801\u70b9\u51fb\u767b\u5f55", "expected": "\u767b\u5f55\u6210\u529f\u8df3\u8f6c\u5230\u9996\u9875"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-002', 'TC-002', '手机验证码登录', '用户管理', 'Web', 'P0', 'Reviewed', 'PASS', '张三', 'REQ-1001', NULL, '[{"action": "\u8f93\u5165\u624b\u673a\u53f7\u83b7\u53d6\u9a8c\u8bc1\u7801", "expected": "\u9a8c\u8bc1\u7801\u53d1\u9001\u6210\u529f"}, {"action": "\u8f93\u5165\u6b63\u786e\u9a8c\u8bc1\u7801\u70b9\u51fb\u767b\u5f55", "expected": "\u767b\u5f55\u6210\u529f"}]', '["\u5192\u70df"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-003', 'TC-003', '第三方微信扫码登录', '用户管理', 'Web', 'P1', 'Reviewed', 'PASS', '李四', 'REQ-1002', NULL, '[{"action": "\u70b9\u51fb\u5fae\u4fe1\u767b\u5f55\u56fe\u6807", "expected": "\u5f39\u51fa\u4e8c\u7ef4\u7801"}, {"action": "\u624b\u673a\u626b\u7801\u786e\u8ba4", "expected": "\u767b\u5f55\u6210\u529f\u8df3\u8f6c\u9996\u9875"}]', '["\u9700\u6c42"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-004', 'TC-004', '密码错误登录失败提示', '用户管理', 'Web', 'P1', 'Reviewed', 'FAIL', '张三', NULL, 'BUG-001', '[{"action": "\u8f93\u5165\u6b63\u786e\u8d26\u53f7\u548c\u9519\u8bef\u5bc6\u7801", "expected": "\u63d0\u793a\u5bc6\u7801\u9519\u8bef"}, {"action": "\u8fde\u7eed5\u6b21\u9519\u8bef\u8f93\u5165", "expected": "\u8d26\u53f7\u88ab\u9501\u5b9a15\u5206\u949f"}]', '["\u56de\u5f52"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-005', 'TC-005', '登录状态过期自动跳转', '用户管理', 'Web', 'P2', 'Draft', 'UNTESTED', '张三', NULL, NULL, NULL, '["\u56de\u5f52"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-register', NULL, '注册功能', '用户管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-006', 'TC-006', '手机号注册新账号', '用户管理', 'Web', 'P0', 'Reviewed', 'PASS', '李四', 'REQ-1003', NULL, '[{"action": "\u8f93\u5165\u624b\u673a\u53f7\u83b7\u53d6\u9a8c\u8bc1\u7801", "expected": "\u9a8c\u8bc1\u7801\u53d1\u9001\u6210\u529f"}, {"action": "\u586b\u5199\u4fe1\u606f\u63d0\u4ea4\u6ce8\u518c", "expected": "\u6ce8\u518c\u6210\u529f\u81ea\u52a8\u767b\u5f55"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-register');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-007', 'TC-007', '邮箱注册新账号', '用户管理', 'Web', 'P1', 'Reviewed', 'PASS', '李四', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-register');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-008', 'TC-008', '重复手机号注册校验', '用户管理', 'Web', 'P1', 'Reviewed', 'FAIL', '李四', NULL, 'BUG-002', NULL, '["\u56de\u5f52"]', 'dir-register');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-perm', NULL, '权限管理', '用户管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-009', 'TC-009', '管理员角色权限验证', '用户管理', 'Web', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u5192\u70df", "\u6743\u9650"]', 'dir-perm');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-010', 'TC-010', '普通用户越权访问拦截', '用户管理', 'API', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u5b89\u5168", "\u56de\u5f52"]', 'dir-perm');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('mod-order', NULL, '订单管理模块', '订单管理', 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, '["\u6838\u5fc3\u6a21\u5757"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-create-order', NULL, '创建订单', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-011', 'TC-011', '正常商品下单流程', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '赵六', 'REQ-2001', NULL, '[{"action": "\u9009\u62e9\u5546\u54c1\u52a0\u5165\u8d2d\u7269\u8f66", "expected": "\u5546\u54c1\u6dfb\u52a0\u6210\u529f"}, {"action": "\u70b9\u51fb\u7ed3\u7b97\u63d0\u4ea4\u8ba2\u5355", "expected": "\u8ba2\u5355\u521b\u5efa\u6210\u529f\u663e\u793a\u5f85\u652f\u4ed8"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-create-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-012', 'TC-012', '库存不足下单提示', '订单管理', 'Web', 'P1', 'Reviewed', 'PASS', '赵六', NULL, NULL, NULL, '["\u56de\u5f52"]', 'dir-create-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-013', 'TC-013', '优惠券叠加使用校验', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', '赵六', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-create-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-payment', NULL, '支付流程', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-014', 'TC-014', '微信支付成功场景', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '赵六', 'REQ-2002', NULL, '[{"action": "\u9009\u62e9\u5fae\u4fe1\u652f\u4ed8", "expected": "\u5524\u8d77\u5fae\u4fe1\u652f\u4ed8"}, {"action": "\u786e\u8ba4\u652f\u4ed8", "expected": "\u652f\u4ed8\u6210\u529f\u8df3\u8f6c\u8ba2\u5355\u8be6\u60c5"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-015', 'TC-015', '支付宝支付成功场景', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '赵六', NULL, NULL, NULL, '["\u5192\u70df"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-016', 'TC-016', '余额不足支付失败', '订单管理', 'Web', 'P1', 'Reviewed', 'FAIL', '张三', NULL, 'BUG-003', NULL, '["\u56de\u5f52"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-017', 'TC-017', '支付超时自动取消订单', '订单管理', 'Web', 'P2', 'PendingReview', 'UNTESTED', '张三', NULL, NULL, NULL, '["\u56de\u5f52"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-refund', NULL, '退款流程', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-018', 'TC-018', '已支付订单申请退款', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '李四', 'REQ-2003', NULL, '[{"action": "\u8fdb\u5165\u8ba2\u5355\u8be6\u60c5\u70b9\u51fb\u7533\u8bf7\u9000\u6b3e", "expected": "\u9000\u6b3e\u7533\u8bf7\u63d0\u4ea4\u6210\u529f"}, {"action": "\u5ba1\u6838\u901a\u8fc7", "expected": "\u9000\u6b3e\u5230\u8d26\u901a\u77e5"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-refund');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-019', 'TC-019', '部分退款金额校验', '订单管理', 'Web', 'P1', 'Reviewed', 'UNTESTED', '李四', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-refund');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('mod-product', NULL, '商品管理模块', '商品管理', 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, '["\u6838\u5fc3\u6a21\u5757"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-search', NULL, '商品搜索', '商品管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-product');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-020', 'TC-020', '关键词精确搜索商品', '商品管理', 'Web', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, '[{"action": "\u8f93\u5165\u5546\u54c1\u540d\u79f0\u641c\u7d22", "expected": "\u8fd4\u56de\u5339\u914d\u7684\u5546\u54c1\u5217\u8868"}]', '["\u5192\u70df"]', 'dir-search');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-021', 'TC-021', '筛选条件组合搜索', '商品管理', 'Web', 'P1', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-search');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-022', 'TC-022', '搜索无结果页面展示', '商品管理', 'Web', 'P2', 'Draft', 'UNTESTED', '王五', NULL, NULL, NULL, '["\u56de\u5f52"]', 'dir-search');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-product-detail', NULL, '商品详情', '商品管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-product');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-023', 'TC-023', '商品详情页信息展示', '商品管理', 'Web', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u5192\u70df"]', 'dir-product-detail');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-024', 'TC-024', '商品评价列表分页', '商品管理', 'Web', 'P2', 'PendingReview', 'UNTESTED', '王五', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-product-detail');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('mod-settings', NULL, '系统设置模块', '系统设置', 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, '["\u57fa\u7840\u6a21\u5757"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-notification', NULL, '通知配置', '系统设置', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-settings');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-025', 'TC-025', '站内消息通知开关', '系统设置', 'Web', 'P2', 'Draft', 'UNTESTED', '赵六', NULL, NULL, NULL, '["\u81ea\u6d4b"]', 'dir-notification');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-026', 'TC-026', '邮件通知模板配置', '系统设置', 'Web', 'P3', 'Draft', 'UNTESTED', '赵六', NULL, NULL, NULL, '["\u81ea\u6d4b"]', 'dir-notification');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-profile', NULL, '个人中心', '系统设置', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-settings');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-027', 'TC-027', '个人信息修改保存', '系统设置', 'Web', 'P1', 'Reviewed', 'PASS', '李四', NULL, NULL, NULL, '["\u81ea\u6d4b"]', 'dir-profile');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-028', 'TC-028', '头像上传与裁剪', '系统设置', 'Web', 'P2', 'PendingReview', 'UNTESTED', '李四', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-profile');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777446852656', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'tc-001');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777447096599', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777447100961', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777447219669', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777449733175', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777450666625', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'tc-001');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777450667553', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'node-1777450666625');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777450723141', NULL, 'fgfggs', NULL, 'General', 'P2', 'Draft', 'PASS', NULL, NULL, NULL, NULL, NULL, 'tc-010');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517187752', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517188135', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517188637', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517189036', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517189422', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517189798', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517190165', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517190507', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517190802', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517191001', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517191191', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517191362', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+
+-- ─── test_plans (1 条) ───────────────
+DELETE FROM `test_plans`;
+INSERT INTO `test_plans` (`id`, `name`, `type`, `case_ids`, `assignee`, `created_at`) VALUES ('p-1778039425608', '1111', 'Requirement', '["tc-002", "tc-006", "tc-009", "tc-010", "node-1777450723141", "tc-011", "tc-014", "tc-015", "tc-018", "tc-020", "tc-023"]', 'ou_9128e3d3d9a4526f6a41bf84dae9c523', '2026-05-06');
+
+-- ─── test_tasks (6 条) ───────────────
+DELETE FROM `test_tasks`;
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-smoke-1', 'v3.0 核心流程冒烟测试', 'RUNNING', 'plan-smoke', '张三', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-reg-1', 'v3.0 全模块回归测试', 'PENDING', 'plan-regression', '李四', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-req-1', 'REQ-2001 订单模块需求测试', 'RUNNING', 'plan-requirement', '赵六', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-self-1', '系统设置模块自测', 'COMPLETED', 'plan-selftest', '王五', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('fb7c9029-06d9-46df-8113-e7c67e69ee2c', '【执行任务】v3.0 ', 'PENDING', 'p-1777445018915', '张三', '2026-04-29T14:43:38.986667');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('c1b2a896-1091-4905-8ddd-c2eaa3e424e3', '【执行任务】1111', 'COMPLETED', 'p-1778039425608', 'ou_9128e3d3d9a4526f6a41bf84dae9c523', '2026-05-06T11:50:25.625136');
+
+-- ─── bugs (0 条) ───────────────
+DELETE FROM `bugs`;
+
+SET FOREIGN_KEY_CHECKS = 1;
\ No newline at end of file
diff --git a/backend/dump_mysql_full.sql b/backend/dump_mysql_full.sql
new file mode 100644
index 0000000..9e3cf95
--- /dev/null
+++ b/backend/dump_mysql_full.sql
@@ -0,0 +1,97 @@
+-- QuantumTest SQLite → MySQL 全量导出(建表 + 数据)
+-- 生成时间: 2026-05-06 12:07:40
+-- 执行前请确保 case_platform 数据库已存在
+
+-- QuantumTest 表结构 DDL (MySQL 8.0+)
+-- 生成时间: 2026-05-06
+-- 执行前请确保已创建数据库: CREATE DATABASE IF NOT EXISTS case_platform DEFAULT CHARACTER SET utf8mb4;
+
+SET NAMES utf8mb4;
+
+
+-- ═══════════════════════════════════════
+-- 数据
+-- ═══════════════════════════════════════
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ─── test_cases (60 条) ───────────────
+DELETE FROM `test_cases`;
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-001', 'TC-001', '账号密码正确登录成功', '用户管理', 'General', 'P2', 'Reviewed', 'UNTESTED', '张三', 'REQ-1001', NULL, '[{"action": "\u6253\u5f00\u767b\u5f55\u9875\u9762", "expected": "\u767b\u5f55\u9875\u6b63\u5e38\u5c55\u793a"}, {"action": "\u8f93\u5165\u6b63\u786e\u7684\u8d26\u53f7\u5bc6\u7801\u70b9\u51fb\u767b\u5f55", "expected": "\u767b\u5f55\u6210\u529f\u8df3\u8f6c\u5230\u9996\u9875"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-002', 'TC-002', '手机验证码登录', '用户管理', 'Web', 'P0', 'Reviewed', 'PASS', '张三', 'REQ-1001', NULL, '[{"action": "\u8f93\u5165\u624b\u673a\u53f7\u83b7\u53d6\u9a8c\u8bc1\u7801", "expected": "\u9a8c\u8bc1\u7801\u53d1\u9001\u6210\u529f"}, {"action": "\u8f93\u5165\u6b63\u786e\u9a8c\u8bc1\u7801\u70b9\u51fb\u767b\u5f55", "expected": "\u767b\u5f55\u6210\u529f"}]', '["\u5192\u70df"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-003', 'TC-003', '第三方微信扫码登录', '用户管理', 'Web', 'P1', 'Reviewed', 'PASS', '李四', 'REQ-1002', NULL, '[{"action": "\u70b9\u51fb\u5fae\u4fe1\u767b\u5f55\u56fe\u6807", "expected": "\u5f39\u51fa\u4e8c\u7ef4\u7801"}, {"action": "\u624b\u673a\u626b\u7801\u786e\u8ba4", "expected": "\u767b\u5f55\u6210\u529f\u8df3\u8f6c\u9996\u9875"}]', '["\u9700\u6c42"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-004', 'TC-004', '密码错误登录失败提示', '用户管理', 'Web', 'P1', 'Reviewed', 'FAIL', '张三', NULL, 'BUG-001', '[{"action": "\u8f93\u5165\u6b63\u786e\u8d26\u53f7\u548c\u9519\u8bef\u5bc6\u7801", "expected": "\u63d0\u793a\u5bc6\u7801\u9519\u8bef"}, {"action": "\u8fde\u7eed5\u6b21\u9519\u8bef\u8f93\u5165", "expected": "\u8d26\u53f7\u88ab\u9501\u5b9a15\u5206\u949f"}]', '["\u56de\u5f52"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-005', 'TC-005', '登录状态过期自动跳转', '用户管理', 'Web', 'P2', 'Draft', 'UNTESTED', '张三', NULL, NULL, NULL, '["\u56de\u5f52"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-register', NULL, '注册功能', '用户管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-006', 'TC-006', '手机号注册新账号', '用户管理', 'Web', 'P0', 'Reviewed', 'PASS', '李四', 'REQ-1003', NULL, '[{"action": "\u8f93\u5165\u624b\u673a\u53f7\u83b7\u53d6\u9a8c\u8bc1\u7801", "expected": "\u9a8c\u8bc1\u7801\u53d1\u9001\u6210\u529f"}, {"action": "\u586b\u5199\u4fe1\u606f\u63d0\u4ea4\u6ce8\u518c", "expected": "\u6ce8\u518c\u6210\u529f\u81ea\u52a8\u767b\u5f55"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-register');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-007', 'TC-007', '邮箱注册新账号', '用户管理', 'Web', 'P1', 'Reviewed', 'PASS', '李四', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-register');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-008', 'TC-008', '重复手机号注册校验', '用户管理', 'Web', 'P1', 'Reviewed', 'FAIL', '李四', NULL, 'BUG-002', NULL, '["\u56de\u5f52"]', 'dir-register');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-perm', NULL, '权限管理', '用户管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-009', 'TC-009', '管理员角色权限验证', '用户管理', 'Web', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u5192\u70df", "\u6743\u9650"]', 'dir-perm');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-010', 'TC-010', '普通用户越权访问拦截', '用户管理', 'API', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u5b89\u5168", "\u56de\u5f52"]', 'dir-perm');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('mod-order', NULL, '订单管理模块', '订单管理', 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, '["\u6838\u5fc3\u6a21\u5757"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-create-order', NULL, '创建订单', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-011', 'TC-011', '正常商品下单流程', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '赵六', 'REQ-2001', NULL, '[{"action": "\u9009\u62e9\u5546\u54c1\u52a0\u5165\u8d2d\u7269\u8f66", "expected": "\u5546\u54c1\u6dfb\u52a0\u6210\u529f"}, {"action": "\u70b9\u51fb\u7ed3\u7b97\u63d0\u4ea4\u8ba2\u5355", "expected": "\u8ba2\u5355\u521b\u5efa\u6210\u529f\u663e\u793a\u5f85\u652f\u4ed8"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-create-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-012', 'TC-012', '库存不足下单提示', '订单管理', 'Web', 'P1', 'Reviewed', 'PASS', '赵六', NULL, NULL, NULL, '["\u56de\u5f52"]', 'dir-create-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-013', 'TC-013', '优惠券叠加使用校验', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', '赵六', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-create-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-payment', NULL, '支付流程', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-014', 'TC-014', '微信支付成功场景', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '赵六', 'REQ-2002', NULL, '[{"action": "\u9009\u62e9\u5fae\u4fe1\u652f\u4ed8", "expected": "\u5524\u8d77\u5fae\u4fe1\u652f\u4ed8"}, {"action": "\u786e\u8ba4\u652f\u4ed8", "expected": "\u652f\u4ed8\u6210\u529f\u8df3\u8f6c\u8ba2\u5355\u8be6\u60c5"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-015', 'TC-015', '支付宝支付成功场景', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '赵六', NULL, NULL, NULL, '["\u5192\u70df"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-016', 'TC-016', '余额不足支付失败', '订单管理', 'Web', 'P1', 'Reviewed', 'FAIL', '张三', NULL, 'BUG-003', NULL, '["\u56de\u5f52"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-017', 'TC-017', '支付超时自动取消订单', '订单管理', 'Web', 'P2', 'PendingReview', 'UNTESTED', '张三', NULL, NULL, NULL, '["\u56de\u5f52"]', 'dir-payment');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-refund', NULL, '退款流程', '订单管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-order');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-018', 'TC-018', '已支付订单申请退款', '订单管理', 'Web', 'P0', 'Reviewed', 'PASS', '李四', 'REQ-2003', NULL, '[{"action": "\u8fdb\u5165\u8ba2\u5355\u8be6\u60c5\u70b9\u51fb\u7533\u8bf7\u9000\u6b3e", "expected": "\u9000\u6b3e\u7533\u8bf7\u63d0\u4ea4\u6210\u529f"}, {"action": "\u5ba1\u6838\u901a\u8fc7", "expected": "\u9000\u6b3e\u5230\u8d26\u901a\u77e5"}]', '["\u5192\u70df", "\u6838\u5fc3\u6d41\u7a0b"]', 'dir-refund');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-019', 'TC-019', '部分退款金额校验', '订单管理', 'Web', 'P1', 'Reviewed', 'UNTESTED', '李四', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-refund');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('mod-product', NULL, '商品管理模块', '商品管理', 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, '["\u6838\u5fc3\u6a21\u5757"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-search', NULL, '商品搜索', '商品管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-product');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-020', 'TC-020', '关键词精确搜索商品', '商品管理', 'Web', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, '[{"action": "\u8f93\u5165\u5546\u54c1\u540d\u79f0\u641c\u7d22", "expected": "\u8fd4\u56de\u5339\u914d\u7684\u5546\u54c1\u5217\u8868"}]', '["\u5192\u70df"]', 'dir-search');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-021', 'TC-021', '筛选条件组合搜索', '商品管理', 'Web', 'P1', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-search');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-022', 'TC-022', '搜索无结果页面展示', '商品管理', 'Web', 'P2', 'Draft', 'UNTESTED', '王五', NULL, NULL, NULL, '["\u56de\u5f52"]', 'dir-search');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-product-detail', NULL, '商品详情', '商品管理', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-product');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-023', 'TC-023', '商品详情页信息展示', '商品管理', 'Web', 'P0', 'Reviewed', 'PASS', '王五', NULL, NULL, NULL, '["\u5192\u70df"]', 'dir-product-detail');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-024', 'TC-024', '商品评价列表分页', '商品管理', 'Web', 'P2', 'PendingReview', 'UNTESTED', '王五', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-product-detail');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('mod-settings', NULL, '系统设置模块', '系统设置', 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, '["\u57fa\u7840\u6a21\u5757"]', NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-notification', NULL, '通知配置', '系统设置', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-settings');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-025', 'TC-025', '站内消息通知开关', '系统设置', 'Web', 'P2', 'Draft', 'UNTESTED', '赵六', NULL, NULL, NULL, '["\u81ea\u6d4b"]', 'dir-notification');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-026', 'TC-026', '邮件通知模板配置', '系统设置', 'Web', 'P3', 'Draft', 'UNTESTED', '赵六', NULL, NULL, NULL, '["\u81ea\u6d4b"]', 'dir-notification');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('dir-profile', NULL, '个人中心', '系统设置', 'Web', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'mod-settings');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-027', 'TC-027', '个人信息修改保存', '系统设置', 'Web', 'P1', 'Reviewed', 'PASS', '李四', NULL, NULL, NULL, '["\u81ea\u6d4b"]', 'dir-profile');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('tc-028', 'TC-028', '头像上传与裁剪', '系统设置', 'Web', 'P2', 'PendingReview', 'UNTESTED', '李四', NULL, NULL, NULL, '["\u9700\u6c42"]', 'dir-profile');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777446852656', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'tc-001');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777447096599', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777447100961', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777447219669', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777449733175', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777450666625', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'tc-001');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777450667553', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, 'node-1777450666625');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777450723141', NULL, 'fgfggs', NULL, 'General', 'P2', 'Draft', 'PASS', NULL, NULL, NULL, NULL, NULL, 'tc-010');
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517187752', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517188135', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517188637', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517189036', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517189422', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517189798', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517190165', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517190507', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517190802', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517191001', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517191191', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `test_cases` (`id`, `case_id`, `text`, `module`, `type`, `priority`, `review_status`, `execution_status`, `maintainer`, `requirement_id`, `bug_id`, `steps`, `tags`, `parent_id`) VALUES ('node-1777517191362', NULL, '新建用例', NULL, 'General', 'P2', 'Draft', 'UNTESTED', NULL, NULL, NULL, NULL, NULL, NULL);
+
+-- ─── test_plans (1 条) ───────────────
+DELETE FROM `test_plans`;
+INSERT INTO `test_plans` (`id`, `name`, `type`, `case_ids`, `assignee`, `created_at`) VALUES ('p-1778039425608', '1111', 'Requirement', '["tc-002", "tc-006", "tc-009", "tc-010", "node-1777450723141", "tc-011", "tc-014", "tc-015", "tc-018", "tc-020", "tc-023"]', 'ou_9128e3d3d9a4526f6a41bf84dae9c523', '2026-05-06');
+
+-- ─── test_tasks (6 条) ───────────────
+DELETE FROM `test_tasks`;
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-smoke-1', 'v3.0 核心流程冒烟测试', 'RUNNING', 'plan-smoke', '张三', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-reg-1', 'v3.0 全模块回归测试', 'PENDING', 'plan-regression', '李四', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-req-1', 'REQ-2001 订单模块需求测试', 'RUNNING', 'plan-requirement', '赵六', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('task-self-1', '系统设置模块自测', 'COMPLETED', 'plan-selftest', '王五', '2026-04-29T12:09:40.318533');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('fb7c9029-06d9-46df-8113-e7c67e69ee2c', '【执行任务】v3.0 ', 'PENDING', 'p-1777445018915', '张三', '2026-04-29T14:43:38.986667');
+INSERT INTO `test_tasks` (`id`, `name`, `status`, `plan_id`, `assignee`, `created_at`) VALUES ('c1b2a896-1091-4905-8ddd-c2eaa3e424e3', '【执行任务】1111', 'COMPLETED', 'p-1778039425608', 'ou_9128e3d3d9a4526f6a41bf84dae9c523', '2026-05-06T11:50:25.625136');
+
+-- ─── bugs (0 条) ───────────────
+DELETE FROM `bugs`;
+
+SET FOREIGN_KEY_CHECKS = 1;
\ No newline at end of file
diff --git a/backend/feishu_client.py b/backend/feishu_client.py
new file mode 100644
index 0000000..4ebe5ee
--- /dev/null
+++ b/backend/feishu_client.py
@@ -0,0 +1,112 @@
+import requests
+import json
+import logging
+
+class FeishuClient:
+ def __init__(self, app_id, app_secret):
+ self.app_id = app_id
+ self.app_secret = app_secret
+ self.base_url = "https://open.feishu.cn/open-apis"
+ self._app_access_token = None
+
+ def get_token(self):
+ url = f"{self.base_url}/auth/v3/app_access_token/internal"
+ payload = {
+ "app_id": self.app_id,
+ "app_secret": self.app_secret
+ }
+ try:
+ resp = requests.post(url, json=payload)
+ resp.raise_for_status()
+ data = resp.json()
+ self._app_access_token = data.get("app_access_token")
+ return self._app_access_token
+ except Exception as e:
+ logging.error(f"Failed to get Feishu token: {e}")
+ return None
+
+ def send_message(self, receive_id_type, receive_id, msg_type, content):
+ token = self.get_token()
+ if not token:
+ return None
+
+ url = f"{self.base_url}/im/v1/messages?receive_id_type={receive_id_type}"
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json"
+ }
+ payload = {
+ "receive_id": receive_id,
+ "msg_type": msg_type,
+ "content": json.dumps(content) if isinstance(content, dict) else content
+ }
+ try:
+ resp = requests.post(url, json=payload, headers=headers)
+ return resp.json()
+ except Exception as e:
+ logging.error(f"Failed to send Feishu message: {e}")
+ return None
+
+ def create_group(self, name, description, user_ids):
+ """
+ Creates a group (chat) and adds users in one call.
+ user_ids should be a list of open_ids.
+ """
+ token = self.get_token()
+ if not token:
+ return None
+
+ url = f"{self.base_url}/im/v1/chats"
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json"
+ }
+ payload = {
+ "name": name,
+ "description": description,
+ "user_id_list": user_ids
+ }
+ try:
+ resp = requests.post(url, json=payload, headers=headers)
+ result = resp.json()
+ print(f"DEBUG create_group response: {result}")
+ return result
+ except Exception as e:
+ logging.error(f"Failed to create Feishu group: {e}")
+ return None
+
+
+ def get_user_ids_by_emails(self, emails):
+ """
+ Retrieves open_ids for a list of emails.
+ """
+ token = self.get_token()
+ if not token:
+ return None
+
+ url = f"{self.base_url}/contact/v3/users/batch_get_id"
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json"
+ }
+ payload = {
+ "emails": emails
+ }
+ try:
+ resp = requests.post(url, json=payload, headers=headers)
+ return resp.json()
+ except Exception as e:
+ logging.error(f"Failed to fetch user IDs: {e}")
+ return None
+
+ def send_card(self, receive_id, card_data):
+
+ """
+ Sends an interactive card message.
+ """
+ return self.send_message(
+ receive_id_type="chat_id" if receive_id.startswith("oc_") else "open_id",
+ receive_id=receive_id,
+ msg_type="interactive",
+ content=card_data
+ )
diff --git a/backend/head.md b/backend/head.md
new file mode 100644
index 0000000..64090de
--- /dev/null
+++ b/backend/head.md
@@ -0,0 +1,22 @@
+孙悦:yue02.sun@d-robotics.cc
+侯志勇:zhiyong.hou@d-robotics.cc
+刘亚琼:yaqiong.liu@d-robotics.cc
+杜凯:kai.du@d-robotics.cc
+路强:qiang01.lu@d-robotics.cc
+张家浩:jiahao01.zhang@d-robotics.cc
+赵晗:han.zhao@d-robotics.cc
+陈适:shi.chen@d-robotics.cc
+付程玲:chengling.fu@d-robotics.cc
+杨祎伟:yiwei.yang@d-robotics.cc
+杨磊:lei01.yang@d-robotics.cc
+辛海丹:haidan.xin@d-robotics.cc
+圣正杰:zhengjie.sheng@d-robotics.cc
+汪明:ming01.wang@d-robotics.cc
+王斌:bin02.wang@d-robotics.cc
+李莹:ying01.li@d-robotics.cc
+周天如:tianru.zhou@d-robotics.cc
+吴龙涛:longtao.wu@d-robotics.cc
+吴超:chao01.wu@d-robotics.cc
+郝建新:jianxin.hao@d-robotics.cc
+董红帅:hongshuai.dong@d-robotics.cc
+陈钦洋:qinyang.chen@d-robotics.cc
\ No newline at end of file
diff --git a/backend/main.py b/backend/main.py
new file mode 100644
index 0000000..e4a5c42
--- /dev/null
+++ b/backend/main.py
@@ -0,0 +1,664 @@
+from fastapi import FastAPI, Depends, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from sqlalchemy.orm import Session
+from typing import List, Optional, Any
+from pydantic import BaseModel
+import uuid, datetime
+
+import models
+from database import engine, get_db
+from feishu_client import FeishuClient
+
+models.Base.metadata.create_all(bind=engine)
+
+# Initialize Feishu Client
+FEISHU_APP_ID = "cli_a9aeb4fb2c78dcb6"
+FEISHU_APP_SECRET = "7nYs724srjEn4jgNPJW9cfuqL4e2OVT6"
+feishu = FeishuClient(FEISHU_APP_ID, FEISHU_APP_SECRET)
+
+
+app = FastAPI(title="QuantumTest Backend")
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+# ─────────────────────────────────────────────
+# Pydantic Schemas
+# ─────────────────────────────────────────────
+
+class TestCaseSchema(BaseModel):
+ id: Optional[str] = None
+ caseId: Optional[str] = None
+ text: Optional[str] = None
+ module: Optional[str] = None
+ type: Optional[str] = None
+ priority: Optional[str] = None
+ reviewStatus: Optional[str] = None
+ executionStatus: Optional[str] = None
+ maintainer: Optional[str] = None
+ requirementId: Optional[str] = None
+ bugId: Optional[str] = None
+ parentId: Optional[str] = None
+ steps: Optional[list] = None
+ tags: Optional[List[str]] = None
+ reviewers: Optional[List[str]] = None
+ children: Optional[List[Any]] = []
+
+
+
+class TaskSchema(BaseModel):
+ id: Optional[str] = None
+ name: str
+ status: Optional[str] = "PENDING"
+ planId: Optional[str] = None
+ assignees: Optional[List[str]] = []
+
+
+
+class TaskStatusUpdate(BaseModel):
+ status: str
+
+
+class TestPlanSchema(BaseModel):
+ id: Optional[str] = None
+ name: str
+ type: str
+ caseIds: Optional[List[str]] = []
+ assignees: Optional[List[str]] = []
+ createdAt: Optional[str] = None
+
+
+
+class BugSchema(BaseModel):
+ id: Optional[str] = None
+ title: str
+ status: Optional[str] = "OPEN"
+ caseId: Optional[str] = None
+
+class BatchReviewSchema(BaseModel):
+ caseIds: List[str]
+ reviewerOpenId: str
+ moduleName: Optional[str] = None
+ message: Optional[str] = "请协助评审以下测试用例"
+
+class BatchUpdateSchema(BaseModel):
+ caseIds: List[str]
+ maintainer: Optional[str] = None
+ reviewers: Optional[List[str]] = None
+
+
+
+
+
+class SpaceSchema(BaseModel):
+ id: Optional[str] = None
+ name: str
+
+
+
+# ─────────────────────────────────────────────
+# Health check
+# ─────────────────────────────────────────────
+
+@app.get("/")
+def read_root():
+ return {"message": "QuantumTest API is running", "version": "2.0"}
+
+
+# ─────────────────────────────────────────────
+# Test Cases
+# ─────────────────────────────────────────────
+
+def _build_tree(cases):
+ case_dict = {}
+ for c in cases:
+ case_dict[c.id] = {
+ "id": c.id,
+ "caseId": c.case_id,
+ "text": c.text,
+ "module": c.module,
+ "type": c.type,
+ "priority": c.priority,
+ "reviewStatus": c.review_status,
+ "executionStatus": c.execution_status,
+ "maintainer": c.maintainer,
+ "requirementId": c.requirement_id,
+ "bugId": c.bug_id,
+ "steps": c.steps,
+ "tags": c.tags,
+ "reviewers": c.reviewers or [],
+ "parentId": c.parent_id,
+ "children": [],
+
+ }
+ root_nodes = []
+ for c in cases:
+ if c.parent_id and c.parent_id in case_dict:
+ case_dict[c.parent_id]["children"].append(case_dict[c.id])
+ else:
+ root_nodes.append(case_dict[c.id])
+ return root_nodes
+
+
+@app.get("/api/cases")
+def get_cases(space_id: str, db: Session = Depends(get_db)):
+ cases = db.query(models.TestCase).filter(models.TestCase.space_id == space_id).all()
+ return {"data": _build_tree(cases)}
+
+
+@app.post("/api/cases")
+def create_case(case: TestCaseSchema, space_id: str, db: Session = Depends(get_db)):
+ existing = db.query(models.TestCase).filter(models.TestCase.id == case.id).first()
+ if existing:
+ return {"message": "already exists", "id": case.id}
+ db_case = models.TestCase(
+ id=case.id,
+ case_id=case.caseId,
+ text=case.text or "新建用例",
+ module=case.module,
+ type=case.type,
+ priority=case.priority,
+ review_status=case.reviewStatus,
+ execution_status=case.executionStatus,
+ maintainer=case.maintainer,
+ requirement_id=case.requirementId,
+ bug_id=case.bugId,
+ parent_id=case.parentId,
+ space_id=space_id,
+ steps=case.steps,
+ tags=case.tags,
+ reviewers=case.reviewers,
+ )
+
+ db.add(db_case)
+ db.commit()
+ return {"message": "success", "id": db_case.id}
+
+
+
+@app.post("/api/cases/batch")
+def batch_create_cases(cases: List[TestCaseSchema], space_id: str, db: Session = Depends(get_db)):
+ for case in cases:
+ db_case = models.TestCase(
+ id=case.id,
+ case_id=case.caseId,
+ text=case.text or "新建用例",
+ module=case.module,
+ type=case.type,
+ priority=case.priority,
+ review_status=case.reviewStatus,
+ execution_status=case.executionStatus,
+ maintainer=case.maintainer,
+ requirement_id=case.requirementId,
+ bug_id=case.bugId,
+ parent_id=case.parentId,
+ space_id=space_id,
+ steps=case.steps,
+ tags=case.tags,
+ reviewers=case.reviewers,
+ )
+
+ db.add(db_case)
+ db.commit()
+ return {"message": "success", "count": len(cases)}
+
+
+@app.post("/api/cases/batch")
+async def batch_update_cases(data: BatchUpdateSchema, db: Session = Depends(get_db)):
+ """
+ Batch update multiple cases (maintainer and/or reviewers).
+ """
+ try:
+ cases = db.query(TestCase).filter(TestCase.id.in_(data.caseIds)).all()
+ for c in cases:
+ if data.maintainer is not None:
+ c.maintainer = data.maintainer
+ if data.reviewers is not None:
+ c.reviewers = data.reviewers
+ db.commit()
+ return {"message": "success", "count": len(cases)}
+ except Exception as e:
+ db.rollback()
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.put("/api/cases/{case_id}")
+
+
+def update_case(case_id: str, case: TestCaseSchema, db: Session = Depends(get_db)):
+ print(f"DEBUG: Updating case {case_id} with data: {case.model_dump(exclude_unset=True)}")
+ db_case = db.query(models.TestCase).filter(models.TestCase.id == case_id).first()
+
+ if not db_case:
+ raise HTTPException(status_code=404, detail="Case not found")
+ if case.text is not None: db_case.text = case.text
+ if case.caseId is not None: db_case.case_id = case.caseId
+ if case.module is not None: db_case.module = case.module
+ if case.type is not None: db_case.type = case.type
+ if case.priority is not None: db_case.priority = case.priority
+ if case.reviewStatus is not None: db_case.review_status = case.reviewStatus
+ if case.executionStatus is not None: db_case.execution_status = case.executionStatus
+ if case.maintainer is not None: db_case.maintainer = case.maintainer
+ if case.requirementId is not None: db_case.requirement_id = case.requirementId
+ if case.bugId is not None: db_case.bug_id = case.bugId
+ if case.steps is not None: db_case.steps = case.steps
+ if case.tags is not None: db_case.tags = case.tags
+ if case.reviewers is not None: db_case.reviewers = case.reviewers
+ db.commit()
+
+ return {"message": "success"}
+
+
+@app.delete("/api/cases/{case_id}")
+def delete_case(case_id: str, db: Session = Depends(get_db)):
+ # Recursively collect all descendant IDs
+ def collect_ids(cid):
+ ids = [cid]
+ children = db.query(models.TestCase).filter(models.TestCase.parent_id == cid).all()
+ for child in children:
+ ids.extend(collect_ids(child.id))
+ return ids
+
+ all_ids = collect_ids(case_id)
+ db.query(models.TestCase).filter(models.TestCase.id.in_(all_ids)).delete(synchronize_session=False)
+ db.commit()
+ return {"message": "success", "deleted": len(all_ids)}
+
+
+# ─────────────────────────────────────────────
+# Test Plans
+# ─────────────────────────────────────────────
+
+@app.get("/api/plans")
+def get_plans(space_id: str, db: Session = Depends(get_db)):
+ plans = db.query(models.TestPlan).filter(models.TestPlan.space_id == space_id).all()
+ return {"data": [
+ {
+ "id": p.id,
+ "name": p.name,
+ "type": p.type,
+ "caseIds": p.case_ids or [],
+ "assignees": p.assignees or [],
+ "createdAt": p.created_at,
+ } for p in plans
+ ]}
+
+
+
+@app.post("/api/plans")
+def create_plan(plan: TestPlanSchema, space_id: str, db: Session = Depends(get_db)):
+ new_id = plan.id or str(uuid.uuid4())
+ db_plan = models.TestPlan(
+ id=new_id,
+ name=plan.name,
+ type=plan.type,
+ case_ids=plan.caseIds or [],
+ assignees=plan.assignees or [],
+ created_at=plan.createdAt or datetime.datetime.now().isoformat(),
+ space_id=space_id
+ )
+ db.add(db_plan)
+ db.commit()
+
+ # --- Feishu Notification ---
+ if plan.assignees:
+ try:
+ # Use global feishu instance initialized at the top
+ type_map = {"Requirement": "需求测试", "Regression": "回归测试", "Self-test": "开发自测", "Smoke": "冒烟测试"}
+ plan_type_cn = type_map.get(plan.type, plan.type)
+
+
+ # Mentions string
+ mentions = " ".join([f"" for uid in plan.assignees])
+
+ card = {
+ "config": {"wide_screen_mode": True},
+ "header": {
+ "title": {"tag": "plain_text", "content": "🚀 新测试计划已创建"},
+ "template": "blue"
+ },
+ "elements": [
+ {
+ "tag": "div",
+ "text": {"tag": "lark_md", "content": f"**计划名称**: {plan.name}\n**计划类型**: {plan_type_cn}\n**包含用例**: {len(plan.caseIds or [])} 条"}
+ },
+ {
+ "tag": "div",
+ "text": {"tag": "lark_md", "content": f"**执行人**: {mentions}"}
+ },
+ {
+ "tag": "hr"
+ },
+ {
+ "tag": "note",
+ "elements": [{"tag": "plain_text", "content": "请相关同学及时关注并开始执行。"}]
+ },
+ {
+ "tag": "action",
+ "actions": [
+ {
+ "tag": "button",
+ "text": {
+ "tag": "plain_text",
+ "content": "查看计划详情"
+ },
+ "type": "primary",
+ "url": f"http://localhost:5173/?view=execution&plan_id={new_id}"
+ }
+ ]
+ }
+ ]
+
+ }
+ # Send to each assignee as a private message or we could create a group.
+ # User said "机器人提醒", usually private message is safer if no group context.
+ for open_id in plan.assignees:
+ print(f"Sending Feishu card to {open_id}...")
+ feishu.send_card(open_id, card)
+ print(f"Successfully sent card to {open_id}")
+ except Exception as e:
+ print(f"❌ Failed to send Feishu notification: {e}")
+ import traceback
+ traceback.print_exc()
+
+ return {"message": "success", "id": new_id}
+
+
+
+
+
+@app.put("/api/plans/{plan_id}")
+def update_plan(plan_id: str, plan: TestPlanSchema, db: Session = Depends(get_db)):
+ db_plan = db.query(models.TestPlan).filter(models.TestPlan.id == plan_id).first()
+ if not db_plan:
+ raise HTTPException(status_code=404, detail="Plan not found")
+ if plan.name: db_plan.name = plan.name
+ if plan.type: db_plan.type = plan.type
+ if plan.caseIds is not None: db_plan.case_ids = plan.caseIds
+ if plan.assignees is not None: db_plan.assignees = plan.assignees
+ db.commit()
+
+ return {"message": "success"}
+
+
+@app.post("/api/plans/{plan_id}/copy")
+def copy_plan(plan_id: str, db: Session = Depends(get_db)):
+ source_plan = db.query(models.TestPlan).filter(models.TestPlan.id == plan_id).first()
+ if not source_plan:
+ raise HTTPException(status_code=404, detail="Plan not found")
+
+ new_id = f"p-copy-{str(uuid.uuid4())[:8]}"
+ db_plan = models.TestPlan(
+ id=new_id,
+ name=f"{source_plan.name} (副本)",
+ type=source_plan.type,
+ case_ids=source_plan.case_ids,
+ assignees=source_plan.assignees,
+ created_at=datetime.datetime.now().isoformat(),
+ space_id=source_plan.space_id
+ )
+ db.add(db_plan)
+ db.commit()
+
+ # Create associated task for the copy
+ new_task_id = str(uuid.uuid4())
+ db_task = models.TestTask(
+ id=new_task_id,
+ name=f"【执行任务】{db_plan.name}",
+ status="PENDING",
+ plan_id=new_id,
+ assignees=db_plan.assignees,
+ created_at=datetime.datetime.now().isoformat()
+ )
+ db.add(db_task)
+ db.commit()
+
+ return {"message": "success", "id": new_id}
+
+
+
+@app.delete("/api/plans/{plan_id}")
+def delete_plan(plan_id: str, db: Session = Depends(get_db)):
+ db_plan = db.query(models.TestPlan).filter(models.TestPlan.id == plan_id).first()
+ if db_plan:
+ db.delete(db_plan)
+ db.commit()
+ return {"message": "success"}
+
+
+# ─────────────────────────────────────────────
+# Test Tasks
+# ─────────────────────────────────────────────
+
+@app.get("/api/tasks")
+def get_tasks(db: Session = Depends(get_db)):
+ tasks = db.query(models.TestTask).all()
+ return {"data": [
+ {
+ "id": t.id,
+ "name": t.name,
+ "status": t.status,
+ "planId": t.plan_id,
+ "assignees": t.assignees or [],
+ "createdAt": t.created_at,
+ } for t in tasks
+ ]}
+
+
+
+@app.post("/api/tasks")
+def create_task(task: TaskSchema, db: Session = Depends(get_db)):
+ new_id = task.id or str(uuid.uuid4())
+ db_task = models.TestTask(
+ id=new_id,
+ name=task.name,
+ status=task.status,
+ plan_id=task.planId,
+ assignees=task.assignees or [],
+ created_at=datetime.datetime.now().isoformat(),
+
+ )
+ db.add(db_task)
+ db.commit()
+ return {"id": new_id}
+
+
+@app.put("/api/tasks/{task_id}/status")
+def update_task_status(task_id: str, body: TaskStatusUpdate, db: Session = Depends(get_db)):
+ db_task = db.query(models.TestTask).filter(models.TestTask.id == task_id).first()
+ if not db_task:
+ raise HTTPException(status_code=404, detail="Task not found")
+ db_task.status = body.status
+ db.commit()
+ return {"message": "success"}
+
+
+# ─────────────────────────────────────────────
+# Bugs
+# ─────────────────────────────────────────────
+
+@app.get("/api/bugs")
+def get_bugs(db: Session = Depends(get_db)):
+ bugs = db.query(models.Bug).all()
+ return {"data": [
+ {"id": b.id, "title": b.title, "status": b.status, "caseId": b.case_id}
+ for b in bugs
+ ]}
+
+
+@app.post("/api/bugs")
+def create_bug(bug: BugSchema, db: Session = Depends(get_db)):
+ new_id = bug.id or f"BUG-{str(uuid.uuid4())[:8].upper()}"
+ db_bug = models.Bug(id=new_id, title=bug.title, status=bug.status or "OPEN", case_id=bug.caseId)
+ db.add(db_bug)
+ db.commit()
+ return {"id": new_id}
+
+
+@app.put("/api/bugs/{bug_id}")
+def update_bug(bug_id: str, bug: BugSchema, db: Session = Depends(get_db)):
+ db_bug = db.query(models.Bug).filter(models.Bug.id == bug_id).first()
+ if not db_bug:
+ raise HTTPException(status_code=404, detail="Bug not found")
+ if bug.status: db_bug.status = bug.status
+ if bug.status: db_bug.status = bug.status
+ db.commit()
+ return {"message": "success"}
+
+@app.delete("/api/bugs/{bug_id}")
+def delete_bug(bug_id: str, db: Session = Depends(get_db)):
+ db_bug = db.query(models.Bug).filter(models.Bug.id == bug_id).first()
+ if db_bug:
+ db.delete(db_bug)
+ db.commit()
+ return {"message": "success"}
+
+
+# ─────────────────────────────────────────────
+# Spaces
+# ─────────────────────────────────────────────
+
+@app.get("/api/spaces")
+def get_spaces(db: Session = Depends(get_db)):
+ spaces = db.query(models.Space).all()
+ return {"data": [{"id": s.id, "name": s.name} for s in spaces]}
+
+
+@app.post("/api/spaces")
+def create_space(space: SpaceSchema, db: Session = Depends(get_db)):
+ new_id = space.id or f"space-{str(uuid.uuid4())[:8]}"
+ db_space = models.Space(id=new_id, name=space.name)
+ db.add(db_space)
+ db.commit()
+ return {"id": new_id, "message": "success"}
+
+
+@app.delete("/api/spaces/{space_id}")
+def delete_space(space_id: str, db: Session = Depends(get_db)):
+ db_space = db.query(models.Space).filter(models.Space.id == space_id).first()
+ if db_space:
+ # 1. 删除该空间下的所有用例
+ db.query(models.TestCase).filter(models.TestCase.space_id == space_id).delete(synchronize_session=False)
+
+ # 2. 删除该空间下的所有测试计划
+ # 注意:如果有测试任务关联到这些计划,可能也需要处理。
+ # 这里先简单删除计划,因为任务没有 space_id 只有 plan_id
+ plans = db.query(models.TestPlan).filter(models.TestPlan.space_id == space_id).all()
+ plan_ids = [p.id for p in plans]
+ if plan_ids:
+ db.query(models.TestTask).filter(models.TestTask.plan_id.in_(plan_ids)).delete(synchronize_session=False)
+ db.query(models.TestPlan).filter(models.TestPlan.id.in_(plan_ids)).delete(synchronize_session=False)
+
+ # 3. 删除空间本身
+ db.delete(db_space)
+ db.commit()
+ return {"message": "success"}
+
+
+# ─────────────────────────────────────────────
+# Feishu Integration & Batch Review
+# ─────────────────────────────────────────────
+
+@app.post("/api/reviews/batch")
+async def batch_review(data: BatchReviewSchema, db: Session = Depends(get_db)):
+ # 1. Fetch case details
+ cases = db.query(models.TestCase).filter(models.TestCase.id.in_(data.caseIds)).all()
+ if not cases:
+ raise HTTPException(status_code=404, detail="No cases found")
+
+ # 2. Collect participants: maintainer + per-case reviewers + initiator
+ participants = set()
+ participants.add(data.reviewerOpenId) # The person who triggered the review
+ for c in cases:
+ if c.maintainer and c.maintainer.startswith("ou_"):
+ participants.add(c.maintainer)
+ if c.reviewers:
+ for r in c.reviewers:
+ if r and r.startswith("ou_"):
+ participants.add(r)
+ participants_list = list(participants)
+ print(f"DEBUG: Batch review participants: {participants_list}")
+
+
+
+ # 3. Create Feishu Group
+ module_part = f"-{data.moduleName}" if data.moduleName else ""
+ group_name = f"用例评审{module_part}-{datetime.datetime.now().strftime('%m%d-%H%M')}"
+
+ group_resp = feishu.create_group(
+ name=group_name,
+ description=f"针对 {len(cases)} 条用例的批量评审群",
+ user_ids=participants_list
+ )
+ print(f"DEBUG: Feishu create_group response: {group_resp}")
+
+
+
+ chat_id = group_resp.get("data", {}).get("chat_id")
+ if not chat_id:
+ # Fallback: if group creation fails (e.g. user not in app contact scope),
+ # try sending to the reviewer directly
+ chat_id = data.reviewerOpenId
+
+ # 4. Prepare & Send Card
+ card_content = {
+ "config": {"wide_screen_mode": True},
+ "header": {
+ "template": "blue",
+ "title": {"content": "📋 测试用例评审邀请", "tag": "plain_text"}
+ },
+ "elements": [
+ {
+ "tag": "div",
+ "text": {"content": f"**发起人:** \n**评审数量:** {len(cases)} 条\n**说明:** {data.message}", "tag": "lark_md"}
+ },
+ {"tag": "hr"},
+ {
+ "tag": "div",
+ "text": {"content": "**待评审列表:**", "tag": "lark_md"}
+ }
+ ]
+ }
+
+ # Add first 5 cases to the card
+ for c in cases[:5]:
+ card_content["elements"].append({
+ "tag": "div",
+ "text": {"content": f"• [{c.priority}] {c.text}", "tag": "plain_text"}
+ })
+
+ if len(cases) > 5:
+ card_content["elements"].append({
+ "tag": "div",
+ "text": {"content": f"... 以及其他 {len(cases)-5} 条用例", "tag": "plain_text"}
+ })
+
+ card_content["elements"].append({
+ "tag": "action",
+ "actions": [
+ {
+ "tag": "button",
+ "text": {"content": "立即去评审", "tag": "plain_text"},
+ "type": "primary",
+ "url": "http://120.48.157.2:55335/static/login.html" # Should be the real URL in prod
+ }
+ ]
+ })
+
+ send_resp = feishu.send_card(chat_id, card_content)
+
+ return {
+ "message": "success",
+ "chat_id": chat_id,
+ "feishu_response": send_resp
+ }
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+
diff --git a/backend/migrate_sqlite_to_mysql.py b/backend/migrate_sqlite_to_mysql.py
new file mode 100644
index 0000000..d473e3c
--- /dev/null
+++ b/backend/migrate_sqlite_to_mysql.py
@@ -0,0 +1,126 @@
+"""
+migrate_sqlite_to_mysql.py
+─────────────────────────────────────────────────────────────────────────────
+将本地 SQLite (quantum_test.db) 的全量数据迁移至百度云 MySQL (case_platform)。
+
+用法:
+ cd backend
+ source venv/bin/activate
+ python migrate_sqlite_to_mysql.py
+
+迁移顺序(严格按依赖关系,避免外键冲突):
+ test_cases → test_plans → test_tasks → bugs
+"""
+
+import json
+from sqlalchemy import create_engine, text
+from sqlalchemy.orm import sessionmaker
+
+# ── 源:SQLite ─────────────────────────────────────────────────────────────
+SQLITE_URL = "sqlite:///./quantum_test.db"
+
+# ── 目标:MySQL ────────────────────────────────────────────────────────────
+MYSQL_URL = (
+ "mysql+pymysql://root_dev:8B1EBC1509cc602b"
+ "@mysql1.rdsmbk3ednsgnnt.rds.bj.baidubce.com:3306"
+ "/case_platform?charset=utf8mb4"
+)
+
+
+def make_session(url, **engine_kwargs):
+ engine = create_engine(url, **engine_kwargs)
+ Session = sessionmaker(bind=engine)
+ return engine, Session()
+
+
+def migrate():
+ print("=" * 60)
+ print("📦 SQLite → MySQL 数据迁移")
+ print("=" * 60)
+
+ # ── 连接两端 ────────────────────────────────────────────────────────────
+ sqlite_engine, src = make_session(
+ SQLITE_URL, connect_args={"check_same_thread": False}
+ )
+ mysql_engine, dst = make_session(
+ MYSQL_URL,
+ pool_pre_ping=True,
+ pool_recycle=1800,
+ )
+
+ # ── 在 MySQL 中建表(如果不存在) ────────────────────────────────────────
+ print("\n[1/5] 在 MySQL 中初始化表结构...")
+ import models
+ models.Base.metadata.create_all(bind=mysql_engine)
+ print(" ✅ 表结构就绪")
+
+ # ── 通用:清空目标表后写入 ───────────────────────────────────────────────
+ def migrate_table(table_name: str, rows):
+ if not rows:
+ print(f" ⚠️ {table_name}: 源表为空,跳过")
+ return
+ # 清空(先删子表,再删父表,顺序由调用方保证)
+ dst.execute(text(f"DELETE FROM {table_name}"))
+ dst.commit()
+
+ inserted = 0
+ for row in rows:
+ data = dict(row._mapping)
+ # JSON 字段在 SQLite 里是字符串,需要反序列化
+ for col in ("steps", "tags", "case_ids"):
+ if col in data and isinstance(data[col], str):
+ try:
+ data[col] = json.loads(data[col])
+ except (json.JSONDecodeError, TypeError):
+ data[col] = None
+ cols = ", ".join(data.keys())
+ placeholders = ", ".join(f":{k}" for k in data.keys())
+ dst.execute(
+ text(f"INSERT INTO {table_name} ({cols}) VALUES ({placeholders})"),
+ data,
+ )
+ inserted += 1
+ dst.commit()
+ print(f" ✅ {table_name}: 迁移 {inserted} 条")
+
+ # ── 读取 SQLite 数据 ─────────────────────────────────────────────────────
+ print("\n[2/5] 读取 SQLite 数据...")
+ cases = src.execute(text("SELECT * FROM test_cases")).fetchall()
+ plans = src.execute(text("SELECT * FROM test_plans")).fetchall() if _table_exists(src, "test_plans") else []
+ tasks = src.execute(text("SELECT * FROM test_tasks")).fetchall() if _table_exists(src, "test_tasks") else []
+ bugs = src.execute(text("SELECT * FROM bugs")).fetchall() if _table_exists(src, "bugs") else []
+
+ print(f" test_cases : {len(cases)} 条")
+ print(f" test_plans : {len(plans)} 条")
+ print(f" test_tasks : {len(tasks)} 条")
+ print(f" bugs : {len(bugs)} 条")
+
+ # ── 迁移(父表先于子表) ─────────────────────────────────────────────────
+ print("\n[3/5] 迁移 test_cases ...")
+ migrate_table("test_cases", cases)
+
+ print("\n[4/5] 迁移 test_plans / test_tasks / bugs ...")
+ migrate_table("test_plans", plans)
+ migrate_table("test_tasks", tasks)
+ migrate_table("bugs", bugs)
+
+ print("\n[5/5] 验证 MySQL 行数...")
+ for tbl in ("test_cases", "test_plans", "test_tasks", "bugs"):
+ count = dst.execute(text(f"SELECT COUNT(*) FROM {tbl}")).scalar()
+ print(f" {tbl:<14}: {count} 条")
+
+ src.close()
+ dst.close()
+ print("\n🎉 迁移完成!")
+
+
+def _table_exists(session, table_name: str) -> bool:
+ """检查 SQLite 中该表是否存在"""
+ result = session.execute(
+ text(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'")
+ ).fetchone()
+ return result is not None
+
+
+if __name__ == "__main__":
+ migrate()
diff --git a/backend/models.py b/backend/models.py
new file mode 100644
index 0000000..9e412a8
--- /dev/null
+++ b/backend/models.py
@@ -0,0 +1,67 @@
+from sqlalchemy import Column, String, JSON
+from database import Base
+
+
+class Space(Base):
+ __tablename__ = "spaces"
+ id = Column(String(50), primary_key=True)
+ name = Column(String(200), nullable=False)
+
+
+class TestCase(Base):
+ __tablename__ = "test_cases"
+
+ id = Column(String(50), primary_key=True)
+ case_id = Column(String(50), unique=True, nullable=True)
+ text = Column(String(200), nullable=False)
+ module = Column(String(100))
+ type = Column(String(50), default="General")
+ priority = Column(String(20), default="P2")
+ review_status = Column(String(50), default="Draft")
+ execution_status = Column(String(50), default="UNTESTED")
+ maintainer = Column(String(100))
+ requirement_id = Column(String(100))
+ bug_id = Column(String(100))
+ steps = Column(JSON, nullable=True) # [{action, expected}]
+ tags = Column(JSON, nullable=True) # string[]
+ reviewers = Column(JSON, nullable=True) # string[] (Feishu open_ids)
+ parent_id = Column(String(50), nullable=True)
+ space_id = Column(String(50))
+
+
+
+
+class TestTask(Base):
+ __tablename__ = "test_tasks"
+
+ id = Column(String(50), primary_key=True)
+ name = Column(String(200), nullable=False)
+ status = Column(String(50), default="PENDING") # PENDING / RUNNING / COMPLETED
+ plan_id = Column(String(50))
+ assignees = Column(JSON, nullable=True) # string[]
+
+ created_at = Column(String(50))
+
+
+class TestPlan(Base):
+ __tablename__ = "test_plans"
+
+ id = Column(String(50), primary_key=True)
+ name = Column(String(200), nullable=False)
+ type = Column(String(50)) # Self-test / Regression / Requirement / Smoke
+ case_ids = Column(JSON, nullable=True) # string[]
+ assignees = Column(JSON, nullable=True) # string[] (Feishu open_ids)
+ created_at = Column(String(50))
+
+ space_id = Column(String(50))
+
+
+
+class Bug(Base):
+ __tablename__ = "bugs"
+
+ id = Column(String(50), primary_key=True)
+ title = Column(String(200), nullable=False)
+ status = Column(String(50), default="OPEN") # OPEN / RESOLVED / CLOSED
+ case_id = Column(String(50))
+
diff --git a/backend/quantum_test.db b/backend/quantum_test.db
new file mode 100644
index 0000000..ebe94ab
Binary files /dev/null and b/backend/quantum_test.db differ
diff --git a/backend/schema_mysql.sql b/backend/schema_mysql.sql
new file mode 100644
index 0000000..3222988
--- /dev/null
+++ b/backend/schema_mysql.sql
@@ -0,0 +1,80 @@
+-- QuantumTest 表结构 DDL (MySQL 8.0+)
+-- 生成时间: 2026-05-06
+-- 执行前请确保已创建数据库: CREATE DATABASE IF NOT EXISTS case_platform DEFAULT CHARACTER SET utf8mb4;
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ─── test_cases ───────────────────────────────────────────────────────────────
+DROP TABLE IF EXISTS `test_cases`;
+CREATE TABLE `test_cases` (
+ `id` VARCHAR(50) NOT NULL COMMENT '节点唯一ID',
+ `case_id` VARCHAR(50) DEFAULT NULL COMMENT '用例编号,如 TC-001',
+ `text` VARCHAR(200) NOT NULL COMMENT '用例标题',
+ `module` VARCHAR(100) DEFAULT NULL COMMENT '所属模块',
+ `type` VARCHAR(50) NOT NULL DEFAULT 'General' COMMENT '类型: General/Web/API/Mobile',
+ `priority` VARCHAR(20) NOT NULL DEFAULT 'P2' COMMENT '优先级: P0/P1/P2/P3',
+ `review_status` VARCHAR(50) NOT NULL DEFAULT 'Draft' COMMENT '评审状态: Draft/PendingReview/Reviewed/Deprecated',
+ `execution_status` VARCHAR(50) NOT NULL DEFAULT 'UNTESTED' COMMENT '执行状态: UNTESTED/PASS/FAIL/BLOCK',
+ `maintainer` VARCHAR(100) DEFAULT NULL COMMENT '维护人(飞书 OpenID 或姓名)',
+ `requirement_id` VARCHAR(100) DEFAULT NULL COMMENT '关联需求ID',
+ `bug_id` VARCHAR(100) DEFAULT NULL COMMENT '关联缺陷ID',
+ `steps` JSON DEFAULT NULL COMMENT '测试步骤 [{action, expected}]',
+ `tags` JSON DEFAULT NULL COMMENT '标签列表 string[]',
+ `parent_id` VARCHAR(50) DEFAULT NULL COMMENT '父节点ID,NULL 表示根节点',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uq_case_id` (`case_id`),
+ KEY `idx_parent_id` (`parent_id`),
+ KEY `idx_module` (`module`),
+ KEY `idx_priority` (`priority`),
+ KEY `idx_review_status` (`review_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='测试用例(树状结构,通过 parent_id 关联父子节点)';
+
+
+-- ─── test_plans ───────────────────────────────────────────────────────────────
+DROP TABLE IF EXISTS `test_plans`;
+CREATE TABLE `test_plans` (
+ `id` VARCHAR(50) NOT NULL COMMENT '计划唯一ID',
+ `name` VARCHAR(200) NOT NULL COMMENT '计划名称',
+ `type` VARCHAR(50) NOT NULL COMMENT '计划类型: Self-test/Regression/Requirement/Smoke',
+ `case_ids` JSON DEFAULT NULL COMMENT '关联用例ID列表 string[]',
+ `assignee` VARCHAR(100) DEFAULT NULL COMMENT '执行人(飞书 OpenID 或姓名)',
+ `created_at` VARCHAR(50) DEFAULT NULL COMMENT '创建时间 ISO8601',
+ PRIMARY KEY (`id`),
+ KEY `idx_type` (`type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='测试计划';
+
+
+-- ─── test_tasks ───────────────────────────────────────────────────────────────
+DROP TABLE IF EXISTS `test_tasks`;
+CREATE TABLE `test_tasks` (
+ `id` VARCHAR(50) NOT NULL COMMENT '任务唯一ID(UUID)',
+ `name` VARCHAR(200) NOT NULL COMMENT '任务名称',
+ `status` VARCHAR(50) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态: PENDING/RUNNING/COMPLETED',
+ `plan_id` VARCHAR(50) DEFAULT NULL COMMENT '关联测试计划ID',
+ `assignee` VARCHAR(100) DEFAULT NULL COMMENT '执行人',
+ `created_at` VARCHAR(50) DEFAULT NULL COMMENT '创建时间 ISO8601',
+ PRIMARY KEY (`id`),
+ KEY `idx_plan_id` (`plan_id`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='测试执行任务';
+
+
+-- ─── bugs ─────────────────────────────────────────────────────────────────────
+DROP TABLE IF EXISTS `bugs`;
+CREATE TABLE `bugs` (
+ `id` VARCHAR(50) NOT NULL COMMENT '缺陷ID,如 BUG-001',
+ `title` VARCHAR(200) NOT NULL COMMENT '缺陷标题',
+ `status` VARCHAR(50) NOT NULL DEFAULT 'OPEN' COMMENT '缺陷状态: OPEN/RESOLVED/CLOSED',
+ `case_id` VARCHAR(50) DEFAULT NULL COMMENT '关联用例ID',
+ PRIMARY KEY (`id`),
+ KEY `idx_case_id` (`case_id`),
+ KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='缺陷记录';
+
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/backend/seed.py b/backend/seed.py
new file mode 100644
index 0000000..72504c1
--- /dev/null
+++ b/backend/seed.py
@@ -0,0 +1,632 @@
+from sqlalchemy.orm import Session
+from database import SessionLocal, engine
+import models
+import datetime
+
+def seed():
+ models.Base.metadata.create_all(bind=engine)
+ db = SessionLocal()
+
+ # Clear existing data for a clean seed
+ db.query(models.TestTask).delete()
+ db.query(models.TestCase).delete()
+ db.commit()
+
+ # ============================================================
+ # 根据 case.md 生成完整的测试用例数据
+ # 模块结构:
+ # - 用户管理模块
+ # - 登录功能
+ # - 注册功能
+ # - 权限管理
+ # - 订单管理模块
+ # - 创建订单
+ # - 支付流程
+ # - 退款流程
+ # - 商品管理模块
+ # - 商品搜索
+ # - 商品详情
+ # - 系统设置模块
+ # - 通知配置
+ # - 个人中心
+ # ============================================================
+
+ mock_cases = [
+ # ==================== 用户管理模块 ====================
+ {
+ "id": "mod-user",
+ "case_id": None,
+ "text": "用户管理模块",
+ "module": "用户管理",
+ "type": "General",
+ "priority": None,
+ "review_status": "Reviewed",
+ "execution_status": "UNTESTED",
+ "maintainer": None,
+ "parent_id": None,
+ "tags": '["核心模块"]'
+ },
+ # -- 登录功能 子目录 --
+ {
+ "id": "dir-login",
+ "case_id": None,
+ "text": "登录功能",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": None,
+ "review_status": "Reviewed",
+ "maintainer": None,
+ "parent_id": "mod-user",
+ "tags": '["登录"]'
+ },
+ {
+ "id": "tc-001",
+ "case_id": "TC-001",
+ "text": "账号密码正确登录成功",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "张三",
+ "requirement_id": "REQ-1001",
+ "parent_id": "dir-login",
+ "steps": '[{"action": "打开登录页面", "expected": "登录页正常展示"}, {"action": "输入正确的账号密码点击登录", "expected": "登录成功跳转到首页"}]',
+ "tags": '["冒烟", "核心流程"]'
+ },
+ {
+ "id": "tc-002",
+ "case_id": "TC-002",
+ "text": "手机验证码登录",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "张三",
+ "requirement_id": "REQ-1001",
+ "parent_id": "dir-login",
+ "steps": '[{"action": "输入手机号获取验证码", "expected": "验证码发送成功"}, {"action": "输入正确验证码点击登录", "expected": "登录成功"}]',
+ "tags": '["冒烟"]'
+ },
+ {
+ "id": "tc-003",
+ "case_id": "TC-003",
+ "text": "第三方微信扫码登录",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "李四",
+ "requirement_id": "REQ-1002",
+ "parent_id": "dir-login",
+ "steps": '[{"action": "点击微信登录图标", "expected": "弹出二维码"}, {"action": "手机扫码确认", "expected": "登录成功跳转首页"}]',
+ "tags": '["需求"]'
+ },
+ {
+ "id": "tc-004",
+ "case_id": "TC-004",
+ "text": "密码错误登录失败提示",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "FAIL",
+ "maintainer": "张三",
+ "bug_id": "BUG-001",
+ "parent_id": "dir-login",
+ "steps": '[{"action": "输入正确账号和错误密码", "expected": "提示密码错误"}, {"action": "连续5次错误输入", "expected": "账号被锁定15分钟"}]',
+ "tags": '["回归"]'
+ },
+ {
+ "id": "tc-005",
+ "case_id": "TC-005",
+ "text": "登录状态过期自动跳转",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P2",
+ "review_status": "Draft",
+ "execution_status": "UNTESTED",
+ "maintainer": "张三",
+ "parent_id": "dir-login",
+ "tags": '["回归"]'
+ },
+ # -- 注册功能 子目录 --
+ {
+ "id": "dir-register",
+ "case_id": None,
+ "text": "注册功能",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-user",
+ },
+ {
+ "id": "tc-006",
+ "case_id": "TC-006",
+ "text": "手机号注册新账号",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "李四",
+ "requirement_id": "REQ-1003",
+ "parent_id": "dir-register",
+ "steps": '[{"action": "输入手机号获取验证码", "expected": "验证码发送成功"}, {"action": "填写信息提交注册", "expected": "注册成功自动登录"}]',
+ "tags": '["冒烟", "核心流程"]'
+ },
+ {
+ "id": "tc-007",
+ "case_id": "TC-007",
+ "text": "邮箱注册新账号",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "李四",
+ "parent_id": "dir-register",
+ "tags": '["需求"]'
+ },
+ {
+ "id": "tc-008",
+ "case_id": "TC-008",
+ "text": "重复手机号注册校验",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "FAIL",
+ "maintainer": "李四",
+ "bug_id": "BUG-002",
+ "parent_id": "dir-register",
+ "tags": '["回归"]'
+ },
+ # -- 权限管理 子目录 --
+ {
+ "id": "dir-perm",
+ "case_id": None,
+ "text": "权限管理",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-user",
+ },
+ {
+ "id": "tc-009",
+ "case_id": "TC-009",
+ "text": "管理员角色权限验证",
+ "module": "用户管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "王五",
+ "parent_id": "dir-perm",
+ "tags": '["冒烟", "权限"]'
+ },
+ {
+ "id": "tc-010",
+ "case_id": "TC-010",
+ "text": "普通用户越权访问拦截",
+ "module": "用户管理",
+ "type": "API",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "王五",
+ "parent_id": "dir-perm",
+ "tags": '["安全", "回归"]'
+ },
+
+ # ==================== 订单管理模块 ====================
+ {
+ "id": "mod-order",
+ "case_id": None,
+ "text": "订单管理模块",
+ "module": "订单管理",
+ "type": "General",
+ "priority": None,
+ "parent_id": None,
+ "tags": '["核心模块"]'
+ },
+ # -- 创建订单 --
+ {
+ "id": "dir-create-order",
+ "case_id": None,
+ "text": "创建订单",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-order",
+ },
+ {
+ "id": "tc-011",
+ "case_id": "TC-011",
+ "text": "正常商品下单流程",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "赵六",
+ "requirement_id": "REQ-2001",
+ "parent_id": "dir-create-order",
+ "steps": '[{"action": "选择商品加入购物车", "expected": "商品添加成功"}, {"action": "点击结算提交订单", "expected": "订单创建成功显示待支付"}]',
+ "tags": '["冒烟", "核心流程"]'
+ },
+ {
+ "id": "tc-012",
+ "case_id": "TC-012",
+ "text": "库存不足下单提示",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "赵六",
+ "parent_id": "dir-create-order",
+ "tags": '["回归"]'
+ },
+ {
+ "id": "tc-013",
+ "case_id": "TC-013",
+ "text": "优惠券叠加使用校验",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P2",
+ "review_status": "Draft",
+ "execution_status": "UNTESTED",
+ "maintainer": "赵六",
+ "parent_id": "dir-create-order",
+ "tags": '["需求"]'
+ },
+ # -- 支付流程 --
+ {
+ "id": "dir-payment",
+ "case_id": None,
+ "text": "支付流程",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-order",
+ },
+ {
+ "id": "tc-014",
+ "case_id": "TC-014",
+ "text": "微信支付成功场景",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "赵六",
+ "requirement_id": "REQ-2002",
+ "parent_id": "dir-payment",
+ "steps": '[{"action": "选择微信支付", "expected": "唤起微信支付"}, {"action": "确认支付", "expected": "支付成功跳转订单详情"}]',
+ "tags": '["冒烟", "核心流程"]'
+ },
+ {
+ "id": "tc-015",
+ "case_id": "TC-015",
+ "text": "支付宝支付成功场景",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "赵六",
+ "parent_id": "dir-payment",
+ "tags": '["冒烟"]'
+ },
+ {
+ "id": "tc-016",
+ "case_id": "TC-016",
+ "text": "余额不足支付失败",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "FAIL",
+ "maintainer": "张三",
+ "bug_id": "BUG-003",
+ "parent_id": "dir-payment",
+ "tags": '["回归"]'
+ },
+ {
+ "id": "tc-017",
+ "case_id": "TC-017",
+ "text": "支付超时自动取消订单",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P2",
+ "review_status": "PendingReview",
+ "execution_status": "UNTESTED",
+ "maintainer": "张三",
+ "parent_id": "dir-payment",
+ "tags": '["回归"]'
+ },
+ # -- 退款流程 --
+ {
+ "id": "dir-refund",
+ "case_id": None,
+ "text": "退款流程",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-order",
+ },
+ {
+ "id": "tc-018",
+ "case_id": "TC-018",
+ "text": "已支付订单申请退款",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "李四",
+ "requirement_id": "REQ-2003",
+ "parent_id": "dir-refund",
+ "steps": '[{"action": "进入订单详情点击申请退款", "expected": "退款申请提交成功"}, {"action": "审核通过", "expected": "退款到账通知"}]',
+ "tags": '["冒烟", "核心流程"]'
+ },
+ {
+ "id": "tc-019",
+ "case_id": "TC-019",
+ "text": "部分退款金额校验",
+ "module": "订单管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "UNTESTED",
+ "maintainer": "李四",
+ "parent_id": "dir-refund",
+ "tags": '["需求"]'
+ },
+
+ # ==================== 商品管理模块 ====================
+ {
+ "id": "mod-product",
+ "case_id": None,
+ "text": "商品管理模块",
+ "module": "商品管理",
+ "type": "General",
+ "priority": None,
+ "parent_id": None,
+ "tags": '["核心模块"]'
+ },
+ {
+ "id": "dir-search",
+ "case_id": None,
+ "text": "商品搜索",
+ "module": "商品管理",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-product",
+ },
+ {
+ "id": "tc-020",
+ "case_id": "TC-020",
+ "text": "关键词精确搜索商品",
+ "module": "商品管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "王五",
+ "parent_id": "dir-search",
+ "steps": '[{"action": "输入商品名称搜索", "expected": "返回匹配的商品列表"}]',
+ "tags": '["冒烟"]'
+ },
+ {
+ "id": "tc-021",
+ "case_id": "TC-021",
+ "text": "筛选条件组合搜索",
+ "module": "商品管理",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "王五",
+ "parent_id": "dir-search",
+ "tags": '["需求"]'
+ },
+ {
+ "id": "tc-022",
+ "case_id": "TC-022",
+ "text": "搜索无结果页面展示",
+ "module": "商品管理",
+ "type": "Web",
+ "priority": "P2",
+ "review_status": "Draft",
+ "execution_status": "UNTESTED",
+ "maintainer": "王五",
+ "parent_id": "dir-search",
+ "tags": '["回归"]'
+ },
+ {
+ "id": "dir-product-detail",
+ "case_id": None,
+ "text": "商品详情",
+ "module": "商品管理",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-product",
+ },
+ {
+ "id": "tc-023",
+ "case_id": "TC-023",
+ "text": "商品详情页信息展示",
+ "module": "商品管理",
+ "type": "Web",
+ "priority": "P0",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "王五",
+ "parent_id": "dir-product-detail",
+ "tags": '["冒烟"]'
+ },
+ {
+ "id": "tc-024",
+ "case_id": "TC-024",
+ "text": "商品评价列表分页",
+ "module": "商品管理",
+ "type": "Web",
+ "priority": "P2",
+ "review_status": "PendingReview",
+ "execution_status": "UNTESTED",
+ "maintainer": "王五",
+ "parent_id": "dir-product-detail",
+ "tags": '["需求"]'
+ },
+
+ # ==================== 系统设置模块 ====================
+ {
+ "id": "mod-settings",
+ "case_id": None,
+ "text": "系统设置模块",
+ "module": "系统设置",
+ "type": "General",
+ "priority": None,
+ "parent_id": None,
+ "tags": '["基础模块"]'
+ },
+ {
+ "id": "dir-notification",
+ "case_id": None,
+ "text": "通知配置",
+ "module": "系统设置",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-settings",
+ },
+ {
+ "id": "tc-025",
+ "case_id": "TC-025",
+ "text": "站内消息通知开关",
+ "module": "系统设置",
+ "type": "Web",
+ "priority": "P2",
+ "review_status": "Draft",
+ "execution_status": "UNTESTED",
+ "maintainer": "赵六",
+ "parent_id": "dir-notification",
+ "tags": '["自测"]'
+ },
+ {
+ "id": "tc-026",
+ "case_id": "TC-026",
+ "text": "邮件通知模板配置",
+ "module": "系统设置",
+ "type": "Web",
+ "priority": "P3",
+ "review_status": "Draft",
+ "execution_status": "UNTESTED",
+ "maintainer": "赵六",
+ "parent_id": "dir-notification",
+ "tags": '["自测"]'
+ },
+ {
+ "id": "dir-profile",
+ "case_id": None,
+ "text": "个人中心",
+ "module": "系统设置",
+ "type": "Web",
+ "priority": None,
+ "parent_id": "mod-settings",
+ },
+ {
+ "id": "tc-027",
+ "case_id": "TC-027",
+ "text": "个人信息修改保存",
+ "module": "系统设置",
+ "type": "Web",
+ "priority": "P1",
+ "review_status": "Reviewed",
+ "execution_status": "PASS",
+ "maintainer": "李四",
+ "parent_id": "dir-profile",
+ "tags": '["自测"]'
+ },
+ {
+ "id": "tc-028",
+ "case_id": "TC-028",
+ "text": "头像上传与裁剪",
+ "module": "系统设置",
+ "type": "Web",
+ "priority": "P2",
+ "review_status": "PendingReview",
+ "execution_status": "UNTESTED",
+ "maintainer": "李四",
+ "parent_id": "dir-profile",
+ "tags": '["需求"]'
+ },
+ ]
+
+ import json
+ for case_data in mock_cases:
+ # Parse JSON strings for steps and tags
+ if "steps" in case_data and isinstance(case_data["steps"], str):
+ case_data["steps"] = json.loads(case_data["steps"])
+ if "tags" in case_data and isinstance(case_data["tags"], str):
+ case_data["tags"] = json.loads(case_data["tags"])
+
+ # Remove None priority for directory nodes
+ if case_data.get("priority") is None:
+ case_data.pop("priority", None)
+
+ db_case = models.TestCase(**case_data)
+ db.add(db_case)
+
+ # ============================================================
+ # 测试任务 - 覆盖4种类型(回归、需求、自测、冒烟)
+ # ============================================================
+ now = datetime.datetime.now().isoformat()
+ mock_tasks = [
+ {
+ "id": "task-smoke-1",
+ "name": "v3.0 核心流程冒烟测试",
+ "status": "RUNNING",
+ "plan_id": "plan-smoke",
+ "assignee": "张三",
+ "created_at": now
+ },
+ {
+ "id": "task-reg-1",
+ "name": "v3.0 全模块回归测试",
+ "status": "PENDING",
+ "plan_id": "plan-regression",
+ "assignee": "李四",
+ "created_at": now
+ },
+ {
+ "id": "task-req-1",
+ "name": "REQ-2001 订单模块需求测试",
+ "status": "RUNNING",
+ "plan_id": "plan-requirement",
+ "assignee": "赵六",
+ "created_at": now
+ },
+ {
+ "id": "task-self-1",
+ "name": "系统设置模块自测",
+ "status": "COMPLETED",
+ "plan_id": "plan-selftest",
+ "assignee": "王五",
+ "created_at": now
+ },
+ ]
+ for task_data in mock_tasks:
+ db_task = models.TestTask(**task_data)
+ db.add(db_task)
+
+ db.commit()
+ print(f"✅ Database seeded: {len(mock_cases)} test cases, {len(mock_tasks)} tasks.")
+ db.close()
+
+
+if __name__ == "__main__":
+ seed()
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..ef614d2
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..68922cb
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..9935fc1
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3528 @@
+{
+ "name": "-",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "-",
+ "version": "0.0.0",
+ "dependencies": {
+ "framer-motion": "^12.38.0",
+ "html-to-image": "^1.11.13",
+ "lucide-react": "^1.11.0",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
+ "reactflow": "^11.11.4",
+ "zustand": "^5.0.12"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.12.2",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.2.1",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.5.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.58.2",
+ "vite": "^8.0.10"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
+ "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
+ "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.127.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
+ "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@reactflow/background": {
+ "version": "11.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
+ "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/background/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reactflow/controls": {
+ "version": "11.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
+ "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/controls/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reactflow/core": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
+ "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3": "^7.4.0",
+ "@types/d3-drag": "^3.0.1",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/core/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reactflow/minimap": {
+ "version": "11.7.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
+ "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/minimap/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reactflow/node-resizer": {
+ "version": "2.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
+ "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.4",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-resizer/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reactflow/node-toolbar": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
+ "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-toolbar/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
+ "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
+ "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
+ "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
+ "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
+ "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
+ "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
+ "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
+ "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
+ "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
+ "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
+ "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
+ "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
+ "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
+ "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
+ "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.7",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.12.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
+ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
+ "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/type-utils": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.59.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
+ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
+ "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.59.1",
+ "@typescript-eslint/types": "^8.59.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
+ "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
+ "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
+ "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
+ "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
+ "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.59.1",
+ "@typescript-eslint/tsconfig-utils": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
+ "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
+ "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.59.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.7"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.23",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz",
+ "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001791",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.344",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz",
+ "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.5.5",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/framer-motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.38.0",
+ "motion-utils": "^12.36.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/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/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
+ "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/html-to-image": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
+ "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz",
+ "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.36.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.36.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "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/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.38",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
+ "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+ "dev": true,
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/reactflow": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
+ "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/background": "11.3.14",
+ "@reactflow/controls": "11.2.14",
+ "@reactflow/core": "11.11.4",
+ "@reactflow/minimap": "11.7.14",
+ "@reactflow/node-resizer": "2.2.14",
+ "@reactflow/node-toolbar": "1.3.14"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
+ "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.127.0",
+ "@rolldown/pluginutils": "1.0.0-rc.17"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.17",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.17",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
+ "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz",
+ "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.59.1",
+ "@typescript-eslint/parser": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.10",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
+ "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.10",
+ "rolldown": "1.0.0-rc.17",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
+ "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bc75709
--- /dev/null
+++ b/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "-",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "framer-motion": "^12.38.0",
+ "html-to-image": "^1.11.13",
+ "lucide-react": "^1.11.0",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
+ "reactflow": "^11.11.4",
+ "zustand": "^5.0.12"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.12.2",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.2.1",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.5.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.58.2",
+ "vite": "^8.0.10"
+ }
+}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons.svg b/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..f90339d
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,184 @@
+.counter {
+ font-size: 16px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ color: var(--accent);
+ background: var(--accent-bg);
+ border: 2px solid transparent;
+ transition: border-color 0.3s;
+ margin-bottom: 24px;
+
+ &:hover {
+ border-color: var(--accent-border);
+ }
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+.hero {
+ position: relative;
+
+ .base,
+ .framework,
+ .vite {
+ inset-inline: 0;
+ margin: 0 auto;
+ }
+
+ .base {
+ width: 170px;
+ position: relative;
+ z-index: 0;
+ }
+
+ .framework,
+ .vite {
+ position: absolute;
+ }
+
+ .framework {
+ z-index: 1;
+ top: 34px;
+ height: 28px;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
+ scale(1.4);
+ }
+
+ .vite {
+ z-index: 0;
+ top: 107px;
+ height: 26px;
+ width: auto;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
+ scale(0.8);
+ }
+}
+
+#center {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ place-content: center;
+ place-items: center;
+ flex-grow: 1;
+
+ @media (max-width: 1024px) {
+ padding: 32px 20px 24px;
+ gap: 18px;
+ }
+}
+
+#next-steps {
+ display: flex;
+ border-top: 1px solid var(--border);
+ text-align: left;
+
+ & > div {
+ flex: 1 1 0;
+ padding: 32px;
+ @media (max-width: 1024px) {
+ padding: 24px 20px;
+ }
+ }
+
+ .icon {
+ margin-bottom: 16px;
+ width: 22px;
+ height: 22px;
+ }
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+
+#docs {
+ border-right: 1px solid var(--border);
+
+ @media (max-width: 1024px) {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+#next-steps ul {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 8px;
+ margin: 32px 0 0;
+
+ .logo {
+ height: 18px;
+ }
+
+ a {
+ color: var(--text-h);
+ font-size: 16px;
+ border-radius: 6px;
+ background: var(--social-bg);
+ display: flex;
+ padding: 6px 12px;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ transition: box-shadow 0.3s;
+
+ &:hover {
+ box-shadow: var(--shadow);
+ }
+ .button-icon {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
+ @media (max-width: 1024px) {
+ margin-top: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ li {
+ flex: 1 1 calc(50% - 8px);
+ }
+
+ a {
+ width: 100%;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+ }
+}
+
+#spacer {
+ height: 88px;
+ border-top: 1px solid var(--border);
+ @media (max-width: 1024px) {
+ height: 48px;
+ }
+}
+
+.ticks {
+ position: relative;
+ width: 100%;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ top: -4.5px;
+ border: 5px solid transparent;
+ }
+
+ &::before {
+ left: 0;
+ border-left-color: var(--border);
+ }
+ &::after {
+ right: 0;
+ border-right-color: var(--border);
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..2e4207e
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,434 @@
+import React, { useEffect } from 'react';
+import { useStore } from './store/useStore';
+
+// Auth
+import LoginPage from './components/auth/LoginPage';
+
+// Layout
+import Sidebar from './components/layout/Sidebar';
+import { ToastContainer, useToastStore } from './components/layout/Toast';
+
+// Editor
+import TableView from './components/editor/TableView';
+import PropertyPanel from './components/editor/PropertyPanel';
+import { ImportModal } from './components/editor/ImportModal';
+
+// Plans
+import PlanListView from './components/plans/PlanListView';
+import TaskExecutionView from './components/plans/TaskExecutionView';
+
+// Shared
+import DashboardView from './components/shared/DashboardView';
+import BugView from './components/shared/BugView';
+
+import { Search, UploadCloud, LogOut } from 'lucide-react';
+
+ const App: React.FC = () => {
+ const {
+ spaces, currentSpaceId, setCurrentSpaceId, viewMode, setViewMode,
+ fetchSpaces, fetchData, fetchTasks, fetchBugs, testTasks, setSelectedTaskId,
+ showImportModal, setShowImportModal, currentUser, setCurrentUser, logout
+ } = useStore();
+
+ const { addToast } = useToastStore();
+
+ useEffect(() => {
+ // fetchSpaces will internally call fetchData after resolving currentSpaceId
+ fetchSpaces();
+ fetchTasks();
+ fetchBugs();
+ }, []);
+
+ // Re-fetch data when user manually switches spaces
+ useEffect(() => {
+ if (currentSpaceId) {
+ fetchData();
+ }
+ }, [currentSpaceId]);
+
+ // Ref to track handled deep links
+ const handledRef = React.useRef(null);
+
+ // Handle deep linking via URL parameters
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const planId = params.get('plan_id');
+ const view = params.get('view');
+
+ if (view === 'execution' && planId) {
+ if (handledRef.current === planId) return;
+
+ const { testTasks, setSelectedTaskId } = useStore.getState();
+ const task = testTasks.find(t => t.planId === planId);
+ if (task) {
+ setSelectedTaskId(task.id);
+ setViewMode('execution');
+ handledRef.current = planId;
+
+ // Clear URL parameters after successful jump
+ const newUrl = window.location.pathname;
+ window.history.replaceState({}, '', newUrl);
+ } else {
+ // If tasks are still loading, don't clear yet, wait for next testTasks update
+ setViewMode('execution');
+ }
+ } else if (view && !handledRef.current) {
+ setViewMode(view as any);
+ handledRef.current = 'mode-only';
+ const newUrl = window.location.pathname;
+ window.history.replaceState({}, '', newUrl);
+ }
+ }, [testTasks]);
+
+
+
+
+
+
+ if (!currentUser) {
+ return setCurrentUser(user)} />;
+ }
+
+ return (
+
+
+ {showImportModal &&
setShowImportModal(false)} />}
+
+ {/* Main Navigation Sidebar */}
+
+
+
+ {/* Header is now inside main content wrapper so sidebar can be full height */}
+
+
+
+
+
+
+
{currentUser.name.slice(-1)}
+
{currentUser.name}
+
+
+
+
+
+
+
+ {/* Dynamic Content Area based on Sidebar selection */}
+
+ {viewMode === 'table' && }
+ {viewMode === 'dashboard' && }
+ {viewMode === 'bugs' && }
+ {viewMode === 'execution' && }
+ {viewMode === 'plans' && }
+
+
+
+ {(viewMode === 'table' || viewMode === 'execution') &&
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/src/assets/hero.png b/src/assets/hero.png
new file mode 100644
index 0000000..02251f4
Binary files /dev/null and b/src/assets/hero.png differ
diff --git a/src/assets/react.svg b/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/vite.svg b/src/assets/vite.svg
new file mode 100644
index 0000000..5101b67
--- /dev/null
+++ b/src/assets/vite.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx
new file mode 100644
index 0000000..1af4782
--- /dev/null
+++ b/src/components/auth/LoginPage.tsx
@@ -0,0 +1,313 @@
+import React, { useState } from 'react';
+import usersData from '../../user.json';
+import { Eye, EyeOff, ShieldCheck, LogIn } from 'lucide-react';
+
+const users = usersData as Record;
+const DEFAULT_PASSWORD = 'admin123';
+
+export interface LoginUser {
+ name: string;
+ openId: string;
+}
+
+interface LoginPageProps {
+ onLogin: (user: LoginUser) => void;
+}
+
+const LoginPage: React.FC = ({ onLogin }) => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ if (!username.trim()) {
+ setError('请输入用户名');
+ return;
+ }
+ if (!users[username]) {
+ setError('用户名不存在');
+ return;
+ }
+ if (password !== DEFAULT_PASSWORD) {
+ setError('密码错误');
+ return;
+ }
+ setLoading(true);
+ setTimeout(() => {
+ onLogin({ name: username, openId: users[username] });
+ setLoading(false);
+ }, 600);
+ };
+
+ return (
+
+
+
+
+
+
Q
+
QuantumTest
+
测试用例管理平台
+
+
+
+
+
默认密码: admin123
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/src/components/editor/ImportModal.tsx b/src/components/editor/ImportModal.tsx
new file mode 100644
index 0000000..4a5ebec
--- /dev/null
+++ b/src/components/editor/ImportModal.tsx
@@ -0,0 +1,224 @@
+import React, { useState, useRef } from 'react';
+import { UploadCloud, X, Image as ImageIcon, Loader2, Sparkles, FileText } from 'lucide-react';
+import { useStore } from '../../store/useStore';
+import { useToastStore } from '../layout/Toast';
+
+const parsedData = {
+ id: `node-ai-${Date.now()}`,
+ text: '推理服务',
+ children: [
+ {
+ id: `node-ai-${Date.now()}-1`,
+ text: '创建推理服务',
+ children: [
+ {
+ id: `node-ai-${Date.now()}-1-1`,
+ text: '点击创建推理服务按钮',
+ steps: [{ action: '点击创建推理服务按钮', expected: '进入创建推理服务页面' }]
+ },
+ {
+ id: `node-ai-${Date.now()}-1-2`,
+ text: '查看页面显示',
+ children: [
+ { id: `node-ai-${Date.now()}-1-2-1`, text: '预期: 左上角显示返回按钮,显示创建推理服务标题' },
+ { id: `node-ai-${Date.now()}-1-2-2`, text: '预期: 下方显示基本信息、推理类型、规格信息等' },
+ { id: `node-ai-${Date.now()}-1-2-3`, text: '预期: 右下角显示取消、确认按钮' }
+ ]
+ },
+ {
+ id: `node-ai-${Date.now()}-1-3`,
+ text: '基本信息',
+ children: [
+ {
+ id: `node-ai-${Date.now()}-1-3-1`,
+ text: '名称',
+ children: [
+ { id: `node-ai-${Date.now()}-1-3-1-1`, text: '备注: 为必填项,不允许重复,不允许为空,支持1-64字符' },
+ { id: `node-ai-${Date.now()}-1-3-1-2`, text: '不输入名称', steps: [{ action: '不输入名称,其他配置合法点击确认', expected: '提示名称不能为空' }] },
+ { id: `node-ai-${Date.now()}-1-3-1-3`, text: '名称重复', steps: [{ action: '输入重复名称', expected: '提示名称已存在' }] },
+ { id: `node-ai-${Date.now()}-1-3-1-4`, text: '名称合法', steps: [{ action: '输入合法名称', expected: '推理服务创建成功' }] },
+ { id: `node-ai-${Date.now()}-1-3-1-5`, text: '输入65位字符', steps: [{ action: '输入65位字符', expected: '第65位字符不允许输入' }] }
+ ]
+ },
+ {
+ id: `node-ai-${Date.now()}-1-3-2`,
+ text: '描述',
+ children: [
+ { id: `node-ai-${Date.now()}-1-3-2-1`, text: '备注: 非必填项,200字符以内' },
+ { id: `node-ai-${Date.now()}-1-3-2-2`, text: '输入为空', steps: [{ action: '输入为空', expected: '创建成功,描述为空' }] },
+ { id: `node-ai-${Date.now()}-1-3-2-3`, text: '输入201字符', steps: [{ action: '输入201字符', expected: '第201位字符不允许输入' }] }
+ ]
+ },
+ {
+ id: `node-ai-${Date.now()}-1-3-3`,
+ text: '标签',
+ children: [
+ { id: `node-ai-${Date.now()}-1-3-3-1`, text: '备注: 最多支持创建20个标签' },
+ { id: `node-ai-${Date.now()}-1-3-3-2`, text: '不输入标签键,输入标签值', steps: [{ action: '点击确认', expected: '给出提示,标签键不能为空' }] }
+ ]
+ }
+ ]
+ },
+ {
+ id: `node-ai-${Date.now()}-1-4`,
+ text: '推理类型',
+ children: [
+ { id: `node-ai-${Date.now()}-1-4-1`, text: '查看类型显示', steps: [{ action: '查看显示', expected: '显示推理服务、分布式推理服务' }] },
+ { id: `node-ai-${Date.now()}-1-4-2`, text: '多次切换选项', steps: [{ action: '多次切换', expected: '仅支持单选,下方规格随之切换' }] }
+ ]
+ },
+ { id: `node-ai-${Date.now()}-1-5`, text: '规格信息' },
+ { id: `node-ai-${Date.now()}-1-6`, text: '基础配置' },
+ { id: `node-ai-${Date.now()}-1-7`, text: '存储配置' }
+ ]
+ },
+ { id: `node-ai-${Date.now()}-2`, text: '搜索推理服务' },
+ { id: `node-ai-${Date.now()}-3`, text: '推理服务列表' }
+ ]
+};
+
+interface ImportModalProps {
+ onClose: () => void;
+}
+
+export const ImportModal: React.FC = ({ onClose }) => {
+ const [isDragging, setIsDragging] = useState(false);
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const fileInputRef = useRef(null);
+
+ const { currentSpaceId, spaceData, importNodes } = useStore();
+ const { addToast } = useToastStore();
+
+ const handleSimulateAI = () => {
+ setIsAnalyzing(true);
+ let current = 0;
+ const interval = setInterval(() => {
+ current += 15;
+ if (current >= 100) {
+ clearInterval(interval);
+ setProgress(100);
+ setTimeout(async () => {
+ // Add parsed data to the tree and persist to backend
+ await importNodes([parsedData]);
+ addToast('AI 识别完毕!已成功导入推理服务用例脑图', 'success');
+ onClose();
+ }, 500);
+ } else {
+ setProgress(current);
+ }
+ }, 400);
+ };
+
+
+ const onFileDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
+ handleSimulateAI();
+ }
+ };
+
+ return (
+
+
e.stopPropagation()}>
+
+
+
+ {!isAnalyzing ? (
+
{ e.preventDefault(); setIsDragging(true); }}
+ onDragLeave={() => setIsDragging(false)}
+ onDrop={onFileDrop}
+ onClick={() => fileInputRef.current?.click()}
+ >
+
+
+
+
点击或将图片拖拽到这里
+
+ 支持解析 XMind 截图、手绘脑图、Excel 截图
+
+
{
+ if (e.target.files && e.target.files.length > 0) {
+ handleSimulateAI();
+ }
+ }}
+ />
+
+ ) : (
+
+
+
+
+
AI 正在解析图片逻辑树...
+
+ 已识别到「推理服务」等 28 个节点
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/components/editor/MindMapView.tsx b/src/components/editor/MindMapView.tsx
new file mode 100644
index 0000000..f829abc
--- /dev/null
+++ b/src/components/editor/MindMapView.tsx
@@ -0,0 +1,618 @@
+import React, { useMemo, useEffect, useRef } from 'react';
+import ReactFlow, {
+ ReactFlowProvider,
+ useReactFlow,
+ Background,
+ Controls,
+ Panel,
+ Handle,
+ Position,
+} from 'reactflow';
+import type { Node, Edge, NodeProps } from 'reactflow';
+import { toPng } from 'html-to-image';
+
+import 'reactflow/dist/style.css';
+import { useStore } from '../../store/useStore';
+import type { TestCaseNode, Priority } from '../../store/useStore';
+
+import { Plus, CornerDownRight, ChevronRight, ChevronDown, Bug, Link, Trash2 } from 'lucide-react';
+import { useToastStore } from '../layout/Toast';
+
+// Custom Node Component
+const MindMapNode = ({ data }: NodeProps) => {
+ const node = data.node as TestCaseNode;
+ const { updateNode, addNode, addSiblingNode, deleteNode, selectedNodeId, setSelectedNodeId, editingNodeId, setEditingNodeId } = useStore();
+ const isSelected = selectedNodeId === node.id;
+ const isEditing = editingNodeId === node.id;
+ const isSet = !node.caseId;
+
+ // Local state for editing — avoids store re-renders breaking the input
+ const [editingText, setEditingText] = React.useState(node.text);
+ React.useEffect(() => {
+ if (isEditing) setEditingText(node.text);
+ }, [isEditing, node.id]);
+
+ const commitEdit = () => {
+ updateNode(node.id, { text: editingText });
+ setEditingNodeId(null);
+ };
+
+ const priorityColors: Record = {
+ P0: '#F53F3F',
+ P1: '#FF7D00',
+ P2: '#F7BA1E',
+ P3: '#165DFF',
+ };
+
+ const statusStyles: Record = {
+ PASS: { border: '#00B42A', bg: '#E8FFFB', color: '#00B42A' },
+ FAIL: { border: '#F53F3F', bg: '#FFECE8', color: '#F53F3F' },
+ BLOCK: { border: '#FF7D00', bg: '#FFF7E8', color: '#FF7D00' },
+ UNTESTED: { border: '#E5E6EB', bg: 'white', color: '#86909C' }
+ };
+
+ const currentStatus = node.executionStatus || 'UNTESTED';
+ const statusStyle = statusStyles[currentStatus];
+
+ return (
+ {
+ e.stopPropagation();
+ setSelectedNodeId(node.id);
+ }}
+ onDoubleClick={(e) => {
+ e.stopPropagation();
+ setEditingNodeId(node.id);
+ }}
+ >
+
+
+
+
+ {node.priority && (
+
+ {node.priority}
+
+ )}
+ {isEditing ? (
+ setEditingText(e.target.value)}
+ onBlur={commitEdit}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') { e.preventDefault(); commitEdit(); }
+ if (e.key === 'Escape') { setEditingText(node.text); setEditingNodeId(null); }
+ }}
+ placeholder=""
+ />
+ ) : (
+ {node.text || '未命名'}
+
+ )}
+
+
+ {(node.requirementId || node.bugId || (node.tags && node.tags.length > 0)) && (
+
+ {node.requirementId && (
+
+ {node.requirementId}
+
+ )}
+ {node.bugId && (
+
+ {node.bugId}
+
+ )}
+ {node.tags && node.tags.map((tag, idx) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {node.children && node.children.length > 0 && (
+
+ )}
+
+
+
+
+ );
+};
+
+const nodeTypes = {
+ mindMap: MindMapNode,
+};
+
+ interface MindMapProps {
+ selectedModuleId?: string | null;
+ onClearModuleSelection?: () => void;
+ executionMode?: boolean;
+ }
+
+ const MindMapInner: React.FC = ({ selectedModuleId, onClearModuleSelection, executionMode }) => {
+
+
+ const { spaceData, currentSpaceId, selectedNodeId, editingNodeId, addNode, addSiblingNode, deleteNode, setEditingNodeId, selectedPlanId, testPlans } = useStore();
+ const testCases = spaceData[currentSpaceId] || [];
+ const { addToast } = useToastStore();
+ const { fitView } = useReactFlow();
+
+ const handleExportImage = () => {
+ const element = document.querySelector('.react-flow__viewport') as HTMLElement;
+ if (!element) return;
+
+ addToast('正在生成图片...', 'info');
+ toPng(document.querySelector('.react-flow') as HTMLElement, {
+ backgroundColor: '#f8fafc',
+ filter: (node) => {
+ // Exclude controls and panels from the export
+ if (node?.classList?.contains('react-flow__controls') || node?.classList?.contains('react-flow__panel')) {
+ return false;
+ }
+ return true;
+ }
+ }).then((dataUrl) => {
+ const link = document.createElement('a');
+ link.download = `mindmap-${currentSpaceId}-${Date.now()}.png`;
+ link.href = dataUrl;
+ link.click();
+ addToast('导出成功', 'success');
+ }).catch((err) => {
+ console.error('Export failed', err);
+ addToast('导出失败', 'error');
+ });
+ };
+
+ const handleAutoLayout = () => {
+ fitView({ duration: 800, padding: 0.2 });
+ addToast('布局已整理', 'success');
+ };
+
+ const activePlanCaseIds = useMemo(() => {
+ if (!selectedPlanId) return null;
+ const plan = testPlans.find(p => p.id === selectedPlanId);
+ return plan ? new Set(plan.caseIds) : null;
+ }, [selectedPlanId, testPlans]);
+
+ // Keyboard shortcuts handler
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const state = useStore.getState();
+ const { selectedNodeId, editingNodeId, addNode, addSiblingNode, deleteNode, setEditingNodeId } = state;
+ if (editingNodeId) return;
+ if (!selectedNodeId) return;
+ const target = e.target as HTMLElement;
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
+ return;
+ }
+
+ switch (e.key) {
+ case 'Tab':
+ e.preventDefault();
+ e.stopPropagation();
+ addNode(selectedNodeId);
+ break;
+ case 'Enter':
+ e.preventDefault();
+ e.stopPropagation();
+ addSiblingNode(selectedNodeId);
+ break;
+ case 'Backspace':
+ case 'Delete':
+ e.preventDefault();
+ e.stopPropagation();
+ deleteNode(selectedNodeId);
+ break;
+ case ' ':
+ case 'F2':
+ e.preventDefault();
+ e.stopPropagation();
+ setEditingNodeId(selectedNodeId);
+ break;
+ case 'Escape':
+ e.preventDefault();
+ e.stopPropagation();
+ setEditingNodeId(null);
+ break;
+ default:
+ break;
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown, true);
+ return () => window.removeEventListener('keydown', handleKeyDown, true);
+ }, []);
+
+ const { nodes, edges } = useMemo(() => {
+ const initialNodes: Node[] = [];
+ const initialEdges: Edge[] = [];
+ const verticalSpacing = 90;
+ const horizontalSpacing = 280;
+
+ const getSubtreeHeight = (node: TestCaseNode): number => {
+ if (!node.children || node.children.length === 0 || node.isExpanded === false) {
+ return verticalSpacing;
+ }
+ return node.children.reduce((acc, child) => acc + getSubtreeHeight(child), 0);
+ };
+
+ const filterTree = (nodes: TestCaseNode[]): TestCaseNode[] => {
+ if (!activePlanCaseIds) return nodes;
+ return nodes
+ .map(node => {
+ const children = node.children ? filterTree(node.children) : [];
+ const isInPlan = activePlanCaseIds.has(node.id);
+ if (isInPlan || children.length > 0) {
+ return { ...node, children };
+ }
+ return null;
+ })
+ .filter((n): n is TestCaseNode => n !== null);
+ };
+
+ const tree = filterTree(testCases);
+ const filteredTestCases = (() => {
+ if (executionMode) {
+ // If in execution mode but plan not loaded/ready, show nothing instead of everything
+ if (!activePlanCaseIds) return [];
+ return tree;
+ }
+ if (!selectedModuleId) return []; // Focus mode: show nothing if not selected
+
+ const findModuleNode = (nodes: TestCaseNode[]): TestCaseNode | null => {
+ for (const node of nodes) {
+ if (node.id === selectedModuleId) return node;
+ if (node.children) {
+ const found = findModuleNode(node.children);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+
+ const modNode = findModuleNode(tree);
+ return modNode ? [modNode] : [];
+ })();
+
+ const traverse = (node: TestCaseNode, x: number, y: number, parentId: string | null = null) => {
+ const currentId = node.id;
+
+ initialNodes.push({
+ id: currentId,
+ type: 'mindMap',
+ data: { node },
+ position: { x, y },
+ });
+
+ if (parentId) {
+ initialEdges.push({
+ id: `edge-${parentId}-${currentId}`,
+ source: parentId,
+ target: currentId,
+ type: 'default',
+ style: { stroke: '#86909C', strokeWidth: 2 },
+ });
+ }
+
+ if (node.children && node.isExpanded !== false) {
+ const nodeHeight = getSubtreeHeight(node);
+ let startY = y - nodeHeight / 2;
+
+ node.children.forEach((child) => {
+ const childHeight = getSubtreeHeight(child);
+ const childY = startY + childHeight / 2;
+ traverse(child, x + horizontalSpacing, childY, currentId);
+ startY += childHeight;
+ });
+ }
+ };
+
+ filteredTestCases.forEach((rootNode) => {
+ // Always center the single root at 0,0 for clean layout
+ traverse(rootNode, 0, 0);
+ });
+
+ return { nodes: initialNodes, edges: initialEdges };
+
+ }, [testCases, activePlanCaseIds, selectedModuleId]);
+
+ return (
+
+
{
+ setEditingNodeId(null);
+ useStore.getState().setSelectedNodeId(null);
+ onClearModuleSelection?.();
+ }}
+
+ fitView
+ fitViewOptions={{ padding: 0.2 }}
+ minZoom={0.1}
+ maxZoom={2}
+ deleteKeyCode={null}
+ selectionKeyCode={null}
+ multiSelectionKeyCode={null}
+ >
+
+
+
+
+
+
+
+
+
+
+ {!executionMode ? (
+ <>
+
Tab 添加子节点
+
Enter 添加同级
+
Delete 删除节点
+
空格/F2 编辑文本
+ >
+ ) : (
+
执行预览模式
+ )}
+
+
+
+
+
+ );
+};
+
+const MindMapView: React.FC = (props) => (
+
+
+
+);
+
+
+
+export default MindMapView;
diff --git a/src/components/editor/PropertyPanel.tsx b/src/components/editor/PropertyPanel.tsx
new file mode 100644
index 0000000..2c9f317
--- /dev/null
+++ b/src/components/editor/PropertyPanel.tsx
@@ -0,0 +1,397 @@
+import React from 'react';
+import { useStore } from '../../store/useStore';
+import type { Priority } from '../../store/useStore';
+import { X, AlertCircle } from 'lucide-react';
+import { UserMentionInput } from './UserMentionInput';
+import { ReviewersInput } from './ReviewersInput';
+
+
+const PropertyPanel: React.FC = () => {
+ const { setSelectedNodeId, updateNode, getSelectedNode } = useStore();
+ const node = getSelectedNode();
+
+ if (!node) return null;
+
+
+
+ return (
+
+ );
+};
+
+export default PropertyPanel;
diff --git a/src/components/editor/ReviewersInput.tsx b/src/components/editor/ReviewersInput.tsx
new file mode 100644
index 0000000..b0963f0
--- /dev/null
+++ b/src/components/editor/ReviewersInput.tsx
@@ -0,0 +1,211 @@
+import React, { useState, useRef, useEffect } from 'react';
+import usersData from '../../user.json';
+import { X, Plus } from 'lucide-react';
+
+const users = usersData as Record;
+
+// For reverse lookup (ID -> Name)
+export const feishuUserMap: Record = Object.entries(users).reduce((acc, [name, id]) => {
+ acc[id] = name;
+ return acc;
+}, {} as Record);
+
+interface ReviewersInputProps {
+ value: string[]; // array of openIds
+ onChange: (value: string[]) => void;
+}
+
+const avatarGradient = (name: string) =>
+ name === '陶航宇' ? 'linear-gradient(135deg, #FF7D00, #F53F3F)' : 'linear-gradient(135deg, #165DFF, #36ABFF)';
+
+export const ReviewersInput: React.FC = ({ value, onChange }) => {
+ const [query, setQuery] = useState('');
+ const [showDropdown, setShowDropdown] = useState(false);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
+ setShowDropdown(false);
+ setQuery('');
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const addReviewer = (id: string) => {
+ if (!value.includes(id)) {
+ onChange([...value, id]);
+ }
+ setQuery('');
+ setShowDropdown(false);
+ };
+
+ const removeReviewer = (id: string) => {
+ onChange(value.filter(v => v !== id));
+ };
+
+ const filteredUsers = Object.entries(users).filter(([name, id]) =>
+ !value.includes(id) && name.toLowerCase().includes(query.toLowerCase())
+ );
+
+ return (
+
+ {/* Selected tags */}
+
+ {value.map(id => {
+ const name = feishuUserMap[id] || id;
+ return (
+
+
+ {name.slice(-2)}
+
+ {name}
+
+
+ );
+ })}
+
+ {/* Add button / input */}
+
+ { setQuery(e.target.value); setShowDropdown(true); }}
+ onFocus={() => setShowDropdown(true)}
+ />
+
+
+
+ {/* Dropdown */}
+ {showDropdown && filteredUsers.length > 0 && (
+
+
选择评审人
+ {filteredUsers.map(([name, id]) => (
+
addReviewer(id)}>
+
{name.slice(-2)}
+
{name}
+
+
+ ))}
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/editor/TableView.tsx b/src/components/editor/TableView.tsx
new file mode 100644
index 0000000..4f70260
--- /dev/null
+++ b/src/components/editor/TableView.tsx
@@ -0,0 +1,842 @@
+import React, { useState, useMemo } from 'react';
+import { useStore } from '../../store/useStore';
+import type { TestCaseNode, Priority } from '../../store/useStore';
+import { useToastStore } from '../layout/Toast';
+import MindMapView from './MindMapView';
+import { ChevronRight, ChevronDown, LayoutGrid, List, FolderOpen, Folder, FileText, Plus, Trash2, CheckCircle, Clock, X } from 'lucide-react';
+import { feishuUserMap } from './UserMentionInput';
+
+import { UserMentionInput } from './UserMentionInput';
+import { ReviewersInput } from './ReviewersInput';
+
+const TableView: React.FC = () => {
+ const {
+ spaces, currentSpaceId, setCurrentSpaceId, addSpace, deleteSpace, spaceData,
+ updateNode, selectedNodeId, setSelectedNodeId, editingNodeId, addNode, addSiblingNode, deleteNode, setShowImportModal, currentUser, batchUpdateNodes
+ } = useStore();
+
+ const [showBatchEditModal, setShowBatchEditModal] = useState(false);
+ const [batchMaintainer, setBatchMaintainer] = useState('');
+ const [batchReviewers, setBatchReviewers] = useState([]);
+
+
+
+ const [displayMode, setDisplayMode] = useState<'table' | 'mindmap'>('table');
+ const [selectedModuleId, setSelectedModuleId] = useState(null);
+ const [expandedModules, setExpandedModules] = useState>(new Set(['root']));
+ const [showSpaceMenu, setShowSpaceMenu] = useState(false);
+ const [showSpaceDialog, setShowSpaceDialog] = useState(false);
+ const [spaceName, setSpaceName] = useState('');
+
+ const currentSpace = spaces.find(s => s.id === currentSpaceId);
+ const testCases = spaceData[currentSpaceId] || [];
+
+ // Batch selection state
+ const [selectedIds, setSelectedIds] = useState>(new Set());
+
+ const moduleNodes = useMemo(() => {
+ return testCases;
+ }, [testCases]);
+
+ const visibleCases = useMemo(() => {
+ if (selectedModuleId === null) return testCases;
+ const find = (nodes: TestCaseNode[]): TestCaseNode | null => {
+ for (const n of nodes) {
+ if (n.id === selectedModuleId) return n;
+ if (n.children) { const r = find(n.children); if (r) return r; }
+ }
+ return null;
+ };
+ const mod = find(testCases);
+ return mod ? [mod] : testCases;
+ }, [testCases, selectedModuleId]);
+
+ const toggleModule = (id: string) => {
+ setExpandedModules(prev => {
+ const next = new Set(prev);
+ next.has(id) ? next.delete(id) : next.add(id);
+ return next;
+ });
+ };
+
+ const handleFeishuReview = async () => {
+ if (!currentUser) {
+ addToast('请先登录', 'error');
+ return;
+ }
+
+ addToast('正在发起飞书评审...', 'info');
+ try {
+ // Resolve module name from selectedModuleId
+ const findNode = (nodes: TestCaseNode[], id: string): TestCaseNode | null => {
+ for (const n of nodes) {
+ if (n.id === id) return n;
+ if (n.children) { const r = findNode(n.children, id); if (r) return r; }
+ }
+ return null;
+ };
+ const moduleNode = selectedModuleId ? findNode(testCases, selectedModuleId) : null;
+ const moduleName = moduleNode ? moduleNode.text : null;
+
+ const response = await fetch('http://localhost:8000/api/reviews/batch', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ caseIds: Array.from(selectedIds),
+ reviewerOpenId: currentUser.openId,
+ moduleName,
+ message: '请各位老板们协助评审这些用例。'
+ })
+ });
+
+ const result = await response.json();
+ if (result.message === 'success') {
+ addToast('飞书评审群已拉起并发送卡片', 'success');
+ setSelectedIds(new Set());
+ } else {
+ addToast('发起评审失败', 'error');
+ }
+ } catch (e) {
+ addToast('网络请求失败', 'error');
+ }
+ };
+
+
+
+ const toggleSelect = (id: string) => {
+ setSelectedIds(prev => {
+ const next = new Set(prev);
+ next.has(id) ? next.delete(id) : next.add(id);
+ return next;
+ });
+ };
+
+ const toggleSelectAll = (nodes: TestCaseNode[]) => {
+ const allIds: string[] = [];
+ const collect = (list: TestCaseNode[]) => {
+ list.forEach(n => {
+ allIds.push(n.id);
+ if (n.children) collect(n.children);
+ });
+ };
+ collect(nodes);
+
+ if (selectedIds.size >= allIds.length) {
+ setSelectedIds(new Set());
+ } else {
+ setSelectedIds(new Set(allIds));
+ }
+ };
+
+ const handleBatchDelete = () => {
+ if (window.confirm(`确定要删除选中的 ${selectedIds.size} 条用例吗?`)) {
+ selectedIds.forEach(id => deleteNode(id));
+ setSelectedIds(new Set());
+ }
+ };
+
+ const handleBatchSetStatus = (status: any) => {
+ selectedIds.forEach(id => updateNode(id, { reviewStatus: status }));
+ setSelectedIds(new Set());
+ };
+
+ const handleBatchUpdate = async () => {
+ if (selectedIds.size === 0) return;
+ const updates: any = {};
+ if (batchMaintainer) updates.maintainer = batchMaintainer;
+ if (batchReviewers.length > 0) updates.reviewers = batchReviewers;
+
+ if (Object.keys(updates).length === 0) {
+ addToast('请至少选择一个修改项', 'info');
+ return;
+ }
+
+ await batchUpdateNodes(Array.from(selectedIds), updates);
+ addToast(`已批量更新 ${selectedIds.size} 条用例`, 'success');
+ setShowBatchEditModal(false);
+ setSelectedIds(new Set());
+ setBatchMaintainer('');
+ setBatchReviewers([]);
+ };
+
+
+
+ const [showModuleDialog, setShowModuleDialog] = useState(false);
+ const [moduleName, setModuleName] = useState('');
+ const [contextMenu, setContextMenu] = useState<{ x: number, y: number, nodeId: string } | null>(null);
+ const { addToast } = useToastStore();
+
+ const handleCreateModule = () => {
+ if (!moduleName.trim()) return;
+ addNode(null, moduleName); // parentId=null adds to root level
+ addToast(`已创建用例集: ${moduleName}`, 'success');
+ setShowModuleDialog(false);
+ setModuleName('');
+ };
+
+ const handleContextMenu = (e: React.MouseEvent, nodeId: string) => {
+ e.preventDefault();
+ setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
+ };
+
+ const closeContextMenu = () => setContextMenu(null);
+
+ const renderDirTree = (nodes: TestCaseNode[], depth: number = 0): React.ReactNode => {
+ return nodes.map(node => {
+ const hasChildren = node.children && node.children.length > 0;
+ const isExpanded = expandedModules.has(node.id);
+ const isSelected = selectedModuleId === node.id;
+
+ return (
+
+ {
+ e.stopPropagation();
+ setSelectedModuleId(isSelected ? null : node.id);
+ if (hasChildren) toggleModule(node.id);
+ }}
+ onContextMenu={(e) => handleContextMenu(e, node.id)}
+ >
+ {hasChildren ? (
+ { e.stopPropagation(); toggleModule(node.id); }}>
+ {isExpanded ? : }
+
+ ) : (
+
+ )}
+
+ {hasChildren ? (isExpanded ? : ) : }
+
+ {editingNodeId === node.id ? (
+ updateNode(node.id, { text: e.target.value })}
+ onBlur={() => useStore.getState().setEditingNodeId(null)}
+ onKeyDown={(e) => e.key === 'Enter' && useStore.getState().setEditingNodeId(null)}
+ onClick={(e) => e.stopPropagation()}
+ />
+ ) : (
+ {node.text}
+ )}
+ {countLeaves(node)}
+
+ {hasChildren && isExpanded && (
+
+ {renderDirTree(node.children!, depth + 1)}
+
+ )}
+
+ );
+ });
+ };
+
+ const countLeaves = (node: TestCaseNode): number => {
+ if (!node.children || node.children.length === 0) return 1;
+ return node.children.reduce((sum, c) => sum + countLeaves(c), 0);
+ };
+
+ const renderRows = (nodes: TestCaseNode[], depth: number = 0): React.ReactNode => {
+ return nodes.map((node) => (
+
+ setSelectedNodeId(node.id)}
+ >
+ |
+ toggleSelect(node.id)}
+ onClick={e => e.stopPropagation()}
+ />
+ |
+
+ {node.caseId || '—'}
+ |
+
+
+
+ {node.children && node.children.length > 0 ? : null}
+
+ updateNode(node.id, { text: e.target.value })}
+ onClick={e => e.stopPropagation()}
+ />
+
+ |
+
+
+ |
+
+ {feishuUserMap[node.maintainer] || node.maintainer || '—'}
+ |
+
+
+ {node.reviewStatus === 'Reviewed' ? '已评审' :
+ node.reviewStatus === 'PendingReview' ? '待评审' :
+ node.reviewStatus === 'Deprecated' ? '已废弃' : '草稿'}
+
+ |
+
+
+
+
+
+
+ {node.id !== 'root' && (
+
+ )}
+
+ |
+
+ {node.children && renderRows(node.children, depth + 1)}
+
+ ));
+ };
+
+ const handleAddSpace = () => {
+ if (!spaceName.trim()) return;
+ addSpace(spaceName);
+ setShowSpaceDialog(false);
+ setSpaceName('');
+ addToast(`已创建用例空间: ${spaceName}`, 'success');
+ };
+
+ return (
+ { closeContextMenu(); setShowSpaceMenu(false); }}>
+
+ {/* Space Selector */}
+
+
{ e.stopPropagation(); setShowSpaceMenu(!showSpaceMenu); }}>
+ {currentSpace?.name}
+
+
+
+ {showSpaceMenu && (
+
e.stopPropagation()}>
+ {spaces.map(space => (
+
{ setCurrentSpaceId(space.id); setShowSpaceMenu(false); }}
+ >
+ {space.name}
+ {spaces.length > 1 && (
+
+ )}
+
+ ))}
+
+
{ setShowSpaceDialog(true); setShowSpaceMenu(false); }}>
+
新建用例空间
+
+
+ )}
+
+
+
+
项目目录
+
+
+
+ {renderDirTree(moduleNodes)}
+
+
+
+
+
+
+
+
+
+
+
+ {selectedIds.size > 0 && (
+
+ 已选 {selectedIds.size} 项
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {!selectedModuleId ? (
+
+
+
请从左侧目录树中选择一个用例集以查看内容
+
+
+ ) : displayMode === 'table' ? (
+
+ ) : (
+
+ setSelectedModuleId(null)}
+ />
+
+ )}
+
+
+
+ {showModuleDialog && (
+
setShowModuleDialog(false)}>
+
e.stopPropagation()}>
+
+
新建用例集
+
+
+
+
+ setModuleName(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleCreateModule()}
+ />
+
+
+
+
+
+
+
+ )}
+
+ {contextMenu && (
+
e.stopPropagation()}
+ >
+
{ useStore.getState().setEditingNodeId(contextMenu.nodeId); closeContextMenu(); }}>
+ 编辑名称
+
+
{
+ if (selectedModuleId === contextMenu.nodeId) setSelectedModuleId(null);
+ deleteNode(contextMenu.nodeId);
+ addToast('已删除目录', 'info');
+ closeContextMenu();
+ }}>
+ 删除目录
+
+
+ )}
+
+ {showSpaceDialog && (
+
setShowSpaceDialog(false)}>
+
e.stopPropagation()}>
+
+
新建用例空间
+
+
+
+
+ setSpaceName(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleAddSpace()}
+ />
+
+
+
+
+
+
+
+
+ )}
+
+ {showBatchEditModal && (
+
setShowBatchEditModal(false)}>
+
e.stopPropagation()}>
+
+
批量修改信息 ({selectedIds.size} 条用例)
+
+
+
+
+
+
+
+
+
+
+
+ 提示:留空则不修改该字段。
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default TableView;
diff --git a/src/components/editor/UserMentionInput.tsx b/src/components/editor/UserMentionInput.tsx
new file mode 100644
index 0000000..d5e066c
--- /dev/null
+++ b/src/components/editor/UserMentionInput.tsx
@@ -0,0 +1,221 @@
+import React, { useState, useRef, useEffect } from 'react';
+import usersData from '../../user.json';
+import { X } from 'lucide-react';
+
+const users = usersData as Record;
+
+// For reverse lookup (ID -> Name)
+export const feishuUserMap: Record = Object.entries(users).reduce((acc, [name, id]) => {
+ acc[id] = name;
+ return acc;
+}, {} as Record);
+
+interface UserMentionInputProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export const UserMentionInput: React.FC = ({ value, onChange, placeholder }) => {
+ const [inputValue, setInputValue] = useState(value);
+ const [showDropdown, setShowDropdown] = useState(false);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ if (feishuUserMap[value] || !value) {
+ setInputValue(value);
+ }
+ }, [value]);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
+ setShowDropdown(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const val = e.target.value;
+ setInputValue(val);
+ onChange(val);
+
+ const searchStr = val.startsWith('@') ? val.slice(1) : val.split('@').pop() || val;
+ const hasMatch = Object.keys(users).some(name => name.toLowerCase().includes(searchStr.toLowerCase()));
+
+ if (val.includes('@')) {
+ setShowDropdown(true);
+ } else {
+ setShowDropdown(val.length > 0 && hasMatch);
+ }
+ };
+
+ const handleSelectUser = (name: string, id: string) => {
+ onChange(id);
+ setInputValue(id);
+ setShowDropdown(false);
+ };
+
+ if (value && feishuUserMap[value]) {
+ return (
+
+
+ {feishuUserMap[value].slice(-2)}
+
+
{feishuUserMap[value]}
+
+
+
+ );
+ }
+
+ const searchStr = inputValue.startsWith('@') ? inputValue.slice(1) : inputValue.split('@').pop() || inputValue;
+ const filteredUsers = Object.entries(users).filter(([name]) =>
+ name.toLowerCase().includes(searchStr.toLowerCase())
+ );
+
+ return (
+
+
{
+ if(inputValue && filteredUsers.length > 0) setShowDropdown(true);
+ }}
+ placeholder={placeholder || ""}
+ />
+
+
+ {showDropdown && filteredUsers.length > 0 && (
+
+
全部人员
+ {filteredUsers.map(([name, id]) => (
+
handleSelectUser(name, id)}
+ >
+
+ {name.slice(-2)}
+
+
+ {name}
+
+
+ ))}
+
+ )}
+
+
+
+ );
+};
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..6afce6e
--- /dev/null
+++ b/src/components/layout/Sidebar.tsx
@@ -0,0 +1,565 @@
+import React, { useState } from 'react';
+import { LayoutDashboard, Library, ClipboardList, Bug, BarChart3, Plus, Tag, Calendar, Search } from 'lucide-react';
+import { useStore } from '../../store/useStore';
+import { UserMentionInput } from '../editor/UserMentionInput';
+import { ReviewersInput, feishuUserMap } from '../editor/ReviewersInput';
+
+
+import type { TestPlanType } from '../../store/useStore';
+
+
+const Sidebar: React.FC = () => {
+ const {
+ spaceData, currentSpaceId, testPlans, testTasks,
+ selectedPlanId, setSelectedPlanId, setSelectedTaskId,
+ addTestPlan, viewMode, setViewMode
+ } = useStore();
+
+ const testCases = spaceData[currentSpaceId] || [];
+ const [showAddModal, setShowAddModal] = useState(false);
+
+ const [newPlanName, setNewPlanName] = useState('');
+ const [newPlanType, setNewPlanType] = useState('Requirement');
+ const [newAssignee, setNewAssignee] = useState([]);
+
+ const [selectedPriorities, setSelectedPriorities] = useState(['P0', 'P1', 'P2', 'P3']);
+ const [newPlanKeyword, setNewPlanKeyword] = useState('');
+ const [selectedModules, setSelectedModules] = useState([]);
+
+ // Collect unique module names from the test case tree
+ const allModules = (() => {
+ const modules = new Set();
+ const traverse = (nodes: any[]) => nodes.forEach(n => {
+ if (n.text && n.id !== 'root') modules.add(n.text);
+ if (n.children) traverse(n.children);
+ });
+ traverse(testCases);
+ return Array.from(modules);
+ })();
+
+ // Collect only meaningful directory nodes (nodes that have children)
+ const topLevelModules = (() => {
+ const result: { id: string; text: string }[] = [];
+ const traverse = (nodes: any[]) => {
+ nodes.forEach(n => {
+ // A directory node: MUST have children to be considered a module/set
+ if (n.children && n.children.length > 0 && n.text) {
+ result.push({ id: n.id, text: n.text });
+ traverse(n.children);
+ }
+ });
+ };
+ traverse(testCases);
+ return result;
+ })();
+
+
+
+
+
+ // Flatten test cases to compute matching cases
+ const getMatchingCaseIds = () => {
+ let ids: string[] = [];
+ const traverse = (nodes: any[], parentModuleId?: string, depth: number = 0, parentMatched: boolean = false) => {
+ nodes.forEach(node => {
+ const moduleId = depth === 0 ? node.id : parentModuleId;
+ const matchesPriority = selectedPriorities.length === 0 || !node.priority || selectedPriorities.includes(node.priority);
+
+ const currentMatchesKeyword = !newPlanKeyword ||
+ node.text.toLowerCase().includes(newPlanKeyword.toLowerCase()) ||
+ (node.tags && node.tags.some((t: string) => t.toLowerCase().includes(newPlanKeyword.toLowerCase())));
+
+ const isMatched = parentMatched || currentMatchesKeyword;
+ const matchesModule = selectedModules.length === 0 || (moduleId && selectedModules.includes(moduleId));
+
+ const isLeaf = !node.children || node.children.length === 0;
+ const isCase = node.caseId || isLeaf;
+
+ if (isCase && matchesPriority && isMatched && matchesModule) {
+ ids.push(node.id);
+ }
+ if (node.children) traverse(node.children, moduleId, depth + 1, isMatched);
+ });
+ };
+ traverse(testCases);
+
+ return ids;
+ };
+
+ const matchingCaseIds = getMatchingCaseIds();
+
+ const handleAddPlan = async () => {
+ if (newPlanName) {
+ await addTestPlan(newPlanName, newPlanType, matchingCaseIds, newAssignee.length > 0 ? newAssignee : undefined);
+ setNewPlanName('');
+ setNewAssignee([]);
+
+ setShowAddModal(false);
+ }
+ };
+
+
+
+ const getPlanIcon = (type: TestPlanType) => {
+ switch (type) {
+ case 'Self-test': return ;
+ case 'Regression': return ;
+ case 'Requirement': return ;
+ default: return ;
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Sidebar;
+
+
+
diff --git a/src/components/layout/Toast.tsx b/src/components/layout/Toast.tsx
new file mode 100644
index 0000000..48e2ad7
--- /dev/null
+++ b/src/components/layout/Toast.tsx
@@ -0,0 +1,108 @@
+import React, { useEffect, useState } from 'react';
+import { create } from 'zustand';
+import { CheckCircle, AlertCircle, Info, X } from 'lucide-react';
+
+type ToastType = 'success' | 'error' | 'info';
+
+interface Toast {
+ id: string;
+ message: string;
+ type: ToastType;
+}
+
+interface ToastState {
+ toasts: Toast[];
+ addToast: (message: string, type?: ToastType) => void;
+ removeToast: (id: string) => void;
+}
+
+export const useToastStore = create((set) => ({
+ toasts: [],
+ addToast: (message, type = 'info') => {
+ const id = Math.random().toString(36).substring(2, 9);
+ set((state) => ({
+ toasts: [...state.toasts, { id, message, type }],
+ }));
+ setTimeout(() => {
+ set((state) => ({
+ toasts: state.toasts.filter((t) => t.id !== id),
+ }));
+ }, 3000);
+ },
+ removeToast: (id) =>
+ set((state) => ({
+ toasts: state.toasts.filter((t) => t.id !== id),
+ })),
+}));
+
+export const ToastContainer: React.FC = () => {
+ const { toasts, removeToast } = useToastStore();
+
+ return (
+
+ {toasts.map((toast) => (
+
+
+ {toast.type === 'success' &&
}
+ {toast.type === 'error' &&
}
+ {toast.type === 'info' &&
}
+
+
{toast.message}
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/plans/PlanListView.tsx b/src/components/plans/PlanListView.tsx
new file mode 100644
index 0000000..fea108b
--- /dev/null
+++ b/src/components/plans/PlanListView.tsx
@@ -0,0 +1,840 @@
+import React, { useState, useMemo } from 'react';
+import { useStore } from '../../store/useStore';
+import type { TestPlanType, TestCaseNode } from '../../store/useStore';
+import { ClipboardList, Plus, Search, MoreHorizontal, Calendar, User, Activity, X, Tag, Trash2 } from 'lucide-react';
+import { UserMentionInput } from '../editor/UserMentionInput';
+import { ReviewersInput, feishuUserMap } from '../editor/ReviewersInput';
+
+
+const PlanListView: React.FC = () => {
+ const {
+ spaceData, currentSpaceId, testPlans, testTasks,
+ addTestPlan, deleteTestPlan, updateTestPlan, copyTestPlan,
+ setViewMode, setSelectedPlanId, setSelectedTaskId
+ } = useStore();
+
+ const testCases = spaceData[currentSpaceId] || [];
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [planName, setPlanName] = useState('');
+ const [planType, setPlanType] = useState('Requirement');
+ const [assignees, setAssignees] = useState([]);
+ const [keyword, setKeyword] = useState('');
+ const [selectedModules, setSelectedModules] = useState([]);
+ const [selectedPriorities, setSelectedPriorities] = useState(['P0', 'P1', 'P2', 'P3']);
+
+ const [editingPlanId, setEditingPlanId] = useState(null);
+ const [showEditModal, setShowEditModal] = useState(false);
+
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [activeMenuId, setActiveMenuId] = useState(null);
+
+ // Close menu when clicking outside
+ React.useEffect(() => {
+ const handleClick = () => setActiveMenuId(null);
+ window.addEventListener('click', handleClick);
+ return () => window.removeEventListener('click', handleClick);
+ }, []);
+
+ const topLevelModules = (() => {
+ const result: { id: string; text: string }[] = [];
+ const traverse = (nodes: any[]) => {
+ nodes.forEach(n => {
+ // Only treat nodes WITH children as modules/directories to avoid redundancy
+ if (n.children && n.children.length > 0 && n.text) {
+ result.push({ id: n.id, text: n.text });
+ traverse(n.children);
+ }
+ });
+ };
+ traverse(testCases);
+ return result;
+ })();
+
+
+ // Compute matching case IDs based on filters
+ const getMatchingCaseIds = () => {
+ const ids: string[] = [];
+ const traverse = (nodes: TestCaseNode[], parentModuleId?: string, depth: number = 0, parentMatched: boolean = false) => {
+ nodes.forEach(node => {
+ const moduleId = depth === 0 ? node.id : parentModuleId;
+
+ // Priority: match if no filter selected, or node matches selected priority, or node has no priority
+ const matchesPriority = selectedPriorities.length === 0 ||
+ !node.priority || selectedPriorities.includes(node.priority);
+
+ // Keyword: match title or tags
+ const currentMatchesKeyword = !keyword ||
+ node.text.toLowerCase().includes(keyword.toLowerCase()) ||
+ (node.tags && node.tags.some(t => t.toLowerCase().includes(keyword.toLowerCase())));
+
+ const isMatched = parentMatched || currentMatchesKeyword;
+
+ // Module: match if no module filter, or node belongs to selected module
+ const matchesModule = selectedModules.length === 0 ||
+ (moduleId && selectedModules.includes(moduleId));
+
+ const isLeaf = !node.children || node.children.length === 0;
+ const isCase = node.caseId || isLeaf;
+
+ if (isCase && matchesPriority && isMatched && matchesModule) {
+ ids.push(node.id);
+ }
+ if (node.children) traverse(node.children, moduleId, depth + 1, isMatched);
+ });
+ };
+ traverse(testCases);
+ return ids;
+ };
+
+
+ const matchingIds = getMatchingCaseIds();
+
+ const handleCreate = async () => {
+ if (!planName.trim()) return;
+ await addTestPlan(planName, planType, matchingIds, assignees.length > 0 ? assignees : undefined);
+ setPlanName('');
+ setAssignees([]);
+
+ setKeyword('');
+ setSelectedModules([]);
+ setSelectedPriorities([]);
+ setShowCreateModal(false);
+ };
+
+ const handleDeletePlan = (e: React.MouseEvent, id: string) => {
+ e.stopPropagation();
+ if (window.confirm('确定要删除这个测试计划吗?相关的执行任务也会被移除。')) {
+ deleteTestPlan(id);
+ }
+ };
+
+ const handleEditOpen = (plan: any) => {
+ setEditingPlanId(plan.id);
+ setPlanName(plan.name);
+ setPlanType(plan.type);
+ setAssignees(plan.assignees || []);
+ setShowEditModal(true);
+ setActiveMenuId(null);
+ };
+
+ const handleSaveEdit = async () => {
+ if (editingPlanId && planName.trim()) {
+ await updateTestPlan(editingPlanId, {
+ name: planName,
+ type: planType,
+ assignees: assignees
+ });
+ setShowEditModal(false);
+ setEditingPlanId(null);
+ setPlanName('');
+ setAssignees([]);
+ }
+ };
+
+ const handleCopy = async (id: string) => {
+ await copyTestPlan(id);
+ setActiveMenuId(null);
+ };
+ const handleOpenTask = (planId: string) => {
+ const task = testTasks.find(t => t.planId === planId);
+
+ if (task) {
+ setSelectedTaskId(task.id);
+ setViewMode('execution');
+ } else {
+ // Fallback if task was not created properly
+ setSelectedPlanId(planId);
+ setViewMode('table');
+ }
+ };
+
+ const filteredPlans = searchQuery
+ ? testPlans.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
+ : testPlans;
+
+ const typeLabels: Record = {
+ 'Requirement': '需求测试',
+ 'Regression': '回归测试',
+ 'Self-test': '自测',
+ 'Smoke': '冒烟测试',
+ };
+
+ const getProgress = (plan: any) => {
+ const total = plan.caseIds?.length || 0;
+ if (total === 0) return { pct: 0, total, executed: 0 };
+
+ let executed = 0;
+ const traverse = (nodes: TestCaseNode[]) => {
+ nodes.forEach(node => {
+ if (plan.caseIds.includes(node.id) && node.executionStatus && node.executionStatus !== 'UNTESTED') {
+ executed++;
+ }
+ if (node.children) traverse(node.children);
+ });
+ };
+ traverse(testCases);
+
+ return { pct: Math.round((executed / total) * 100), total, executed };
+ };
+
+ return (
+
+
+
+
测试计划
+ {testPlans.length}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+
+
+ {filteredPlans.map(plan => {
+ const progress = getProgress(plan);
+ return (
+
handleOpenTask(plan.id)}>
+
+
+
+
+
{typeLabels[plan.type] || plan.type}
+
+
+
+ {activeMenuId === plan.id && (
+
+
{ e.stopPropagation(); handleEditOpen(plan); }}>编辑计划
+
{ e.stopPropagation(); handleCopy(plan.id); }}>复制计划
+
{ e.stopPropagation(); handleDeletePlan(e, plan.id); }}>删除计划
+
+
+ )}
+
+
+
+
+
+
+
{plan.name}
+
+
+
+ {plan.createdAt?.split('T')[0]}
+
+
+
+
+
+ 执行进度
+ {progress.executed}/{progress.total} ({progress.pct}%)
+
+
+
+
+
+
+
{plan.caseIds?.length || 0} 用例
+
+
+
+ );
+ })}
+
+ {filteredPlans.length === 0 && (
+
+
+
{searchQuery ? '没有匹配的计划' : '暂无测试计划,点击右上角创建'}
+
+ )}
+
+
+ {/* ========== Create Plan Modal ========== */}
+ {showCreateModal && (
+
setShowCreateModal(false)}>
+
e.stopPropagation()}>
+
+
创建测试计划
+
+
+
+
+
+
+ setPlanName(e.target.value)}
+ autoFocus
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {topLevelModules.map(m => (
+
+ ))}
+
+
+
+
+
+
+
+ setKeyword(e.target.value)}
+ />
+
+
+
+
+
+
+ {['P0', 'P1', 'P2', 'P3'].map(p => (
+
+ ))}
+
+
+
+
+ 已匹配到 {matchingIds.length} 条相关用例
+
+
+
+
+
+
+
+
+
+ )}
+
+ {showEditModal && (
+
setShowEditModal(false)}>
+
e.stopPropagation()}>
+
+
编辑测试计划
+
+
+
+
+
+ setPlanName(e.target.value)}
+ autoFocus
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default PlanListView;
diff --git a/src/components/plans/TaskExecutionView.tsx b/src/components/plans/TaskExecutionView.tsx
new file mode 100644
index 0000000..1c5b0fb
--- /dev/null
+++ b/src/components/plans/TaskExecutionView.tsx
@@ -0,0 +1,411 @@
+import React, { useState, useEffect } from 'react';
+import { useStore } from '../../store/useStore';
+import { ChevronLeft, CheckCircle, XCircle, Ban, Play, Save, Bug, LayoutGrid, List } from 'lucide-react';
+import { useToastStore } from '../layout/Toast';
+import MindMapView from '../editor/MindMapView';
+
+const TaskExecutionView: React.FC = () => {
+ const { spaceData, currentSpaceId, testTasks, selectedTaskId, setViewMode, testPlans, updateNode, updateTaskStatus, addBug } = useStore();
+ const testCases = spaceData[currentSpaceId] || [];
+ const { addToast } = useToastStore();
+
+ const task = testTasks.find(t => t.id === selectedTaskId);
+ const plan = testPlans.find(p => p.id === task?.planId);
+
+ const [displayMode, setDisplayMode] = useState<'list' | 'mindmap'>('list');
+
+ useEffect(() => {
+ if (plan && useStore.getState().selectedPlanId !== plan.id) {
+ useStore.getState().setSelectedPlanId(plan.id);
+ }
+ }, [plan]);
+
+ // 查找属于该计划的用例
+ const getPlanCases = () => {
+ // Robust search for the plan if testPlans is still loading
+ const currentPlan = plan || testPlans.find(p => p.id === (task?.planId || useStore.getState().selectedPlanId));
+ if (!currentPlan || !currentPlan.caseIds) return [];
+
+ const results: any[] = [];
+ const targetIds = new Set(currentPlan.caseIds);
+
+ // Search across all spaceData if currentSpaceId is not set yet
+ const allData = currentSpaceId ? [testCases] : Object.values(spaceData);
+
+ const traverse = (nodes: any[]) => {
+ nodes.forEach(node => {
+ if (targetIds.has(node.id)) {
+ results.push(node);
+ }
+ if (node.children) traverse(node.children);
+ });
+ };
+
+ allData.forEach(tree => traverse(tree));
+ return results;
+ };
+
+
+ const planCases = getPlanCases();
+
+ // If we have plan but no cases found, and currentSpaceId is empty, we might need a sync
+ useEffect(() => {
+ if (plan && !currentSpaceId && Object.keys(spaceData).length > 0) {
+ // Try to find which space this plan belongs to
+ // (Simplified: for now just fetch data if empty)
+ }
+ }, [plan, currentSpaceId, spaceData]);
+
+
+ if (!task) return 任务不存在
;
+
+ return (
+
+
+
+ {displayMode === 'list' ? (
+
+
+
用例列表 ({planCases.length})
+
+ {planCases.map(c => (
+
+
+ {c.text}
+
+ ))}
+
+
+
+
+
+ {planCases.map(c => (
+
+
+
+ {c.priority}
+
{c.text}
+
+
+
+
+
+
+
+
+ {c.steps && c.steps.length > 0 && (
+
+
执行步骤
+
+ {c.steps.map((s: any, idx: number) => (
+
+ {idx + 1}
+ {s.action}
+ {s.expected}
+
+ ))}
+
+
+ )}
+
+
+ {c.executionStatus === 'FAIL' && !c.bugId && (
+
+ )}
+ {c.bugId && (
+
+
+ 已关联 Bug: {c.bugId}
+
+ )}
+
+
+ ))}
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default TaskExecutionView;
diff --git a/src/components/shared/BugView.tsx b/src/components/shared/BugView.tsx
new file mode 100644
index 0000000..013a62d
--- /dev/null
+++ b/src/components/shared/BugView.tsx
@@ -0,0 +1,245 @@
+import React from 'react';
+import { useStore } from '../../store/useStore';
+import { Bug as BugIcon, CheckCircle, Clock } from 'lucide-react';
+
+const BugView: React.FC = () => {
+ const { bugs, deleteBug } = useStore();
+
+ const [selectedBug, setSelectedBug] = React.useState(null);
+
+ const handleDelete = (id: string) => {
+ if (window.confirm('确定要删除这个 Bug 记录吗?')) {
+ deleteBug(id);
+ }
+ };
+
+ return (
+
+
+
缺陷管理 (Bug Tracker)
+
+
+ 总计: {bugs.length}
+
+
+ 处理中: {bugs.filter(b => b.status === 'OPEN').length}
+
+
+
+
+
+ {bugs.length === 0 ? (
+
当前没有记录任何 Bug。
+ ) : (
+
+
+
+ | Bug ID |
+ 标题 |
+ 关联用例 |
+ 状态 |
+ 操作 |
+
+
+
+ {bugs.map(bug => (
+
+ | {bug.id} |
+ {bug.title} |
+ {bug.caseId} |
+
+
+ {bug.status === 'OPEN' ? '未解决' :
+ bug.status === 'RESOLVED' ? '已修复' : '已关闭'}
+
+ |
+
+
+
+
+
+ |
+
+ ))}
+
+
+ )}
+
+
+ {/* Bug Details Modal */}
+ {selectedBug && (
+
setSelectedBug(null)}>
+
e.stopPropagation()}>
+
+
缺陷详情 - {selectedBug.id}
+
+
+
+
+
+
{selectedBug.title}
+
+
+
+
{selectedBug.caseId}
+
+
+
+
+
+ {selectedBug.status === 'OPEN' ? '未解决' :
+ selectedBug.status === 'RESOLVED' ? '已修复' : '已关闭'}
+
+
+
+
+
+
{selectedBug.description || '暂无详细描述'}
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default BugView;
diff --git a/src/components/shared/DashboardView.tsx b/src/components/shared/DashboardView.tsx
new file mode 100644
index 0000000..cafe26c
--- /dev/null
+++ b/src/components/shared/DashboardView.tsx
@@ -0,0 +1,456 @@
+import React from 'react';
+import { useStore } from '../../store/useStore';
+import type { TestCaseNode } from '../../store/useStore';
+
+import { BarChart3, PieChart, TrendingUp, CheckCircle2, XCircle, AlertCircle } from 'lucide-react';
+
+const DashboardView: React.FC = () => {
+ const { spaceData, currentSpaceId, testTasks, setViewMode, setSelectedTaskId, currentUser } = useStore();
+
+ const testCases = spaceData[currentSpaceId] || [];
+
+ const handleOpenTask = (taskId: string) => {
+ setSelectedTaskId(taskId);
+ setViewMode('execution');
+ };
+
+
+
+ // Simple aggregation logic
+ const getStats = (nodes: TestCaseNode[]) => {
+ let stats = { total: 0, P0: 0, P1: 0, P2: 0, P3: 0, pass: 0, fail: 0, untested: 0 };
+
+ const traverse = (items: TestCaseNode[]) => {
+ items.forEach(node => {
+ stats.total++;
+ if (node.priority) stats[node.priority]++;
+ if (node.executionStatus === 'PASS') stats.pass++;
+ else if (node.executionStatus === 'FAIL') stats.fail++;
+ else stats.untested++;
+
+
+ if (node.children) traverse(node.children);
+ });
+ };
+
+ traverse(nodes);
+ return stats;
+ };
+
+ const stats = getStats(testCases);
+ const passRate = stats.total > 0 ? Math.round((stats.pass / stats.total) * 100) : 0;
+
+ return (
+
+
+
+
+ 总用例数
+
+
+
{stats.total}
+
+
+ 较上周 +12%
+
+
+
+
+
+ 通过率
+
+
+
{passRate}%
+
+
+
+
+
+
+
+
+
执行分布 (Execution Distribution)
+
+
+
+
+ 已通过 (Pass)
+
+
+
0 ? (stats.pass/stats.total)*100 : 0}%` }}>
+
{stats.pass}
+
+
+
+
+
+ 未通过 (Fail)
+
+
+
0 ? (stats.fail/stats.total)*100 : 0}%` }}>
+
{stats.fail}
+
+
+
+
+
+
0 ? (stats.untested/stats.total)*100 : 0}%` }}>
+
{stats.untested}
+
+
+
+
+
+
+
优先级分布 (Priority Analysis)
+
+ {['P0', 'P1', 'P2', 'P3'].map(p => {
+ const count = stats[p as keyof typeof stats] as number;
+ const percentage = stats.total > 0 ? (count / stats.total) * 100 : 0;
+ return (
+
+ );
+ })}
+
+
+
+
+
通过率概览 (Pass Rate Donut)
+
+
+
+
通过: {stats.pass}
+
失败: {stats.fail}
+
未执行: {stats.untested}
+
+
+
+
+
+
任务看板 (Task Board)
+
+ {testTasks.length === 0 ? (
+
暂无执行任务
+ ) : (
+ testTasks.map(task => (
+
handleOpenTask(task.id)}
+ >
+
+ {task.name}
+ {((task.assignee && task.assignee.startsWith('ou_')) || !task.assignee) ? (currentUser?.name || '未知用户') : task.assignee} · {task.createdAt?.split('T')[0]}
+
+
+
+ {task.status === 'RUNNING' && '运行中'}
+ {task.status === 'PENDING' && '未运行'}
+ {task.status === 'COMPLETED' && '已完成'}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default DashboardView;
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..5f8ac58
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,134 @@
+:root {
+ --primary: #0066FF;
+ --primary-hover: #0052CC;
+ --bg-main: #F4F7FB;
+ --bg-surface: #FFFFFF;
+ --text-main: #1D2129;
+ --text-secondary: #4E5969;
+ --border-color: #E5E6EB;
+ --p0-color: #F53F3F;
+ --p1-color: #FF7D00;
+ --p2-color: #F7BA1E;
+ --p3-color: #165DFF;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
+ --transition-base: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ background-color: var(--bg-main);
+ color: var(--text-main);
+ overflow: hidden;
+}
+
+#root {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Scrollbar Styling */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #C9CDD4;
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #86909C;
+}
+
+/* Glassmorphism Classes */
+.glass {
+ background: rgba(255, 255, 255, 0.7);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.card {
+ background: var(--bg-surface);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border-color);
+}
+
+/* Global Button & Interactive Feedback */
+button,
+[role="button"],
+.clickable,
+.interactive-item,
+.dir-item,
+.global-nav-list li,
+.plan-list li,
+.table-row,
+.tool-btn,
+.toggle-btn,
+.action-btn {
+ transition: var(--transition-base);
+ cursor: pointer;
+ user-select: none;
+}
+
+button:active,
+[role="button"]:active,
+.clickable:active,
+.interactive-item:active,
+.dir-item:active,
+.global-nav-list li:active,
+.plan-list li:active,
+.tool-btn:active,
+.toggle-btn:active,
+.action-btn:active {
+ transform: scale(0.97);
+ opacity: 0.9;
+}
+
+.table-row:active {
+ background-color: #F0F7FF !important;
+}
+
+/* Base button styles to ensure consistency */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 8px 16px;
+ border-radius: var(--radius-md);
+ font-weight: 500;
+ font-size: 14px;
+ border: 1px solid transparent;
+ transition: var(--transition-base);
+}
+
+.btn-primary {
+ background: var(--primary);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: var(--primary-hover);
+ box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
+}
+
+.btn-primary:active {
+ transform: scale(0.96);
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/src/store/useStore.ts b/src/store/useStore.ts
new file mode 100644
index 0000000..4ef7378
--- /dev/null
+++ b/src/store/useStore.ts
@@ -0,0 +1,670 @@
+import { create } from 'zustand';
+
+export type Priority = 'P0' | 'P1' | 'P2' | 'P3';
+export type TestPlanType = 'Self-test' | 'Regression' | 'Requirement' | 'Smoke';
+
+export interface TestPlan {
+ id: string;
+ name: string;
+ type: TestPlanType;
+ createdAt: string;
+ caseIds?: string[];
+ assignees?: string[];
+}
+
+
+export interface TestTask {
+ id: string;
+ name: string;
+ status: 'PENDING' | 'RUNNING' | 'COMPLETED';
+ planId: string;
+ assignees: string[];
+ createdAt: string;
+}
+
+export interface Bug {
+
+ id: string;
+ title: string;
+ status: 'OPEN' | 'RESOLVED' | 'CLOSED';
+ caseId: string;
+}
+
+export interface TestCaseNode {
+ id: string;
+ caseId?: string; // e.g., TC-001
+ text: string; // Title
+ module?: string; // Module category
+ type?: 'Web' | 'Mobile' | 'API' | 'General';
+ priority?: Priority;
+ children?: TestCaseNode[];
+ steps?: { action: string; expected: string }[];
+ isExpanded?: boolean;
+ executionStatus?: 'PASS' | 'FAIL' | 'BLOCK' | 'UNTESTED';
+ reviewStatus?: 'Draft' | 'PendingReview' | 'Reviewed' | 'Deprecated';
+ maintainer?: string;
+
+ requirementId?: string;
+ bugId?: string;
+ tags?: string[];
+ reviewers?: string[]; // Array of openIds for Feishu group review
+}
+
+
+
+
+
+
+export interface TestReport {
+ id: string;
+ planId: string;
+ total: number;
+ pass: number;
+ fail: number;
+ date: string;
+}
+
+export interface Space {
+ id: string;
+ name: string;
+}
+
+interface AppState {
+ viewMode: 'mindmap' | 'table' | 'dashboard' | 'bugs' | 'execution' | 'plans';
+ spaces: Space[];
+ currentSpaceId: string;
+ spaceData: { [spaceId: string]: TestCaseNode[] };
+
+ testPlans: TestPlan[];
+ testTasks: TestTask[];
+ selectedPlanId: string | null;
+ selectedNodeId: string | null;
+ editingNodeId: string | null;
+ selectedTaskId: string | null;
+ showImportModal: boolean;
+ currentUser: { name: string; openId: string } | null;
+
+ // Space Actions
+ addSpace: (name: string) => void;
+ deleteSpace: (id: string) => void;
+ setCurrentSpaceId: (id: string) => void;
+
+ setViewMode: (mode: 'mindmap' | 'table' | 'dashboard' | 'bugs' | 'execution' | 'plans') => void;
+ fetchSpaces: () => Promise;
+ fetchData: () => Promise;
+
+ fetchTasks: () => Promise;
+ fetchBugs: () => Promise;
+ importNodes: (nodes: any[]) => Promise;
+ addTask: (task: Partial) => Promise;
+
+ updateTaskStatus: (taskId: string, status: TestTask['status']) => Promise;
+
+ setSelectedNodeId: (id: string | null) => void;
+ setEditingNodeId: (id: string | null) => void;
+ setSelectedTaskId: (id: string | null) => void;
+ setSelectedPlanId: (id: string | null) => void;
+ setShowImportModal: (show: boolean) => void;
+ setCurrentUser: (user: { name: string; openId: string } | null) => void;
+ logout: () => void;
+
+ addTestPlan: (name: string, type: TestPlanType, caseIds?: string[], assignees?: string[]) => void;
+
+
+ updateNode: (id: string, updates: Partial) => void;
+ updateNodeSteps: (id: string, steps: { action: string; expected: string }[]) => void;
+ addNode: (parentId: string | null, title?: string) => void;
+ addSiblingNode: (id: string, title?: string) => void;
+ deleteNode: (id: string) => void;
+ addBug: (caseId: string, title: string) => void;
+ deleteBug: (id: string) => void;
+ deleteTestPlan: (id: string) => void;
+ updateTestPlan: (id: string, updates: Partial) => Promise;
+ copyTestPlan: (id: string) => Promise;
+ batchUpdateNodes: (ids: string[], updates: Partial) => Promise;
+
+
+ getSelectedNode: () => TestCaseNode | null;
+
+}
+
+
+
+
+const initialData: TestCaseNode[] = [];
+
+
+
+
+export const useStore = create((set, get) => ({
+ viewMode: 'dashboard',
+ spaces: [],
+ currentSpaceId: '',
+
+ spaceData: {},
+
+
+ testPlans: [],
+ testTasks: [],
+ bugs: [],
+
+ selectedPlanId: null,
+ selectedNodeId: null,
+ editingNodeId: null,
+ selectedTaskId: null,
+ showImportModal: false,
+ currentUser: (() => {
+ try {
+ const saved = localStorage.getItem('quantum_user');
+ return saved ? JSON.parse(saved) : null;
+ } catch {
+ return null;
+ }
+ })(),
+
+
+ fetchSpaces: async () => {
+ try {
+ const res = await fetch('http://localhost:8000/api/spaces');
+ const data = await res.json();
+ if (data.data && data.data.length > 0) {
+ const existingId = get().currentSpaceId;
+ const validId = data.data.find((s: {id: string}) => s.id === existingId)
+ ? existingId
+ : data.data[0].id;
+ set({ spaces: data.data, currentSpaceId: validId });
+ // Auto-fetch cases for the resolved space
+ await get().fetchData();
+ } else if (data.data) {
+ set({ spaces: data.data, currentSpaceId: '' });
+ }
+ } catch (e) {
+ console.error('Failed to fetch spaces', e);
+ }
+ },
+
+ addSpace: async (name) => {
+ try {
+ const res = await fetch('http://localhost:8000/api/spaces', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name })
+ });
+ const data = await res.json();
+ if (data.id) {
+ set(state => ({
+ spaces: [...state.spaces, { id: data.id, name }],
+ currentSpaceId: data.id,
+ spaceData: { ...state.spaceData, [data.id]: [] }
+ }));
+ }
+ } catch (e) {
+ console.error('Failed to add space', e);
+ }
+ },
+
+ deleteSpace: async (id) => {
+ try {
+ await fetch(`http://localhost:8000/api/spaces/${id}`, { method: 'DELETE' });
+ set(state => {
+ const newSpaces = state.spaces.filter(s => s.id !== id);
+ const newCurrentId = state.currentSpaceId === id ? (newSpaces[0]?.id || '') : state.currentSpaceId;
+ return { spaces: newSpaces, currentSpaceId: newCurrentId };
+ });
+ } catch (e) {
+ console.error('Failed to delete space', e);
+ }
+ },
+
+
+ setCurrentSpaceId: (currentSpaceId) => set({ currentSpaceId }),
+
+ fetchData: async () => {
+ const sid = get().currentSpaceId;
+ if (!sid) return;
+ try {
+ const [casesRes, plansRes] = await Promise.all([
+ fetch(`http://localhost:8000/api/cases?space_id=${sid}`),
+ fetch(`http://localhost:8000/api/plans?space_id=${sid}`),
+ ]);
+
+
+ const casesData = await casesRes.json();
+ const plansData = await plansRes.json();
+
+ set(state => ({
+ // 只要后端返回了 data 数组(即使是空数组),就覆盖本地状态,避免 fallback 到 initialData
+ spaceData: (casesData && Array.isArray(casesData.data))
+ ? { ...state.spaceData, [sid]: casesData.data }
+ : state.spaceData,
+ testPlans: (plansData && Array.isArray(plansData.data))
+ ? plansData.data
+ : state.testPlans,
+ }));
+
+ } catch (e) {
+ console.warn('[DB] fetchData failed, using local state', e);
+ }
+ },
+
+ fetchTasks: async () => {
+ try {
+ const response = await fetch('http://localhost:8000/api/tasks');
+ const data = await response.json();
+ set({ testTasks: data.data || [] });
+ } catch (e) {
+ console.error('Failed to fetch tasks', e);
+ }
+ },
+
+ fetchBugs: async () => {
+ try {
+ const response = await fetch('http://localhost:8000/api/bugs');
+ const data = await response.json();
+ set({ bugs: data.data || [] });
+ } catch (e) {
+ console.error('Failed to fetch bugs', e);
+ }
+ },
+
+
+ addTask: async (task) => {
+ try {
+ const response = await fetch('http://localhost:8000/api/tasks', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(task)
+ });
+ const result = await response.json();
+ if (result.id) {
+ const fullTask = { ...task, id: result.id, createdAt: new Date().toISOString() } as TestTask;
+ set(state => ({ testTasks: [...state.testTasks, fullTask] }));
+ }
+ } catch (e) {
+ console.warn('Failed to add task to backend, falling back to local state');
+ const fullTask = { ...task, id: `task-${Date.now()}`, createdAt: new Date().toISOString() } as TestTask;
+ set(state => ({ testTasks: [...state.testTasks, fullTask] }));
+ }
+ },
+
+ updateTaskStatus: async (taskId, status) => {
+ try {
+ await fetch(`http://localhost:8000/api/tasks/${taskId}/status`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status })
+ });
+ set(state => ({
+ testTasks: state.testTasks.map(t => t.id === taskId ? { ...t, status } : t)
+ }));
+ } catch (e) {
+ console.error('Failed to update task status', e);
+ }
+ },
+
+ setViewMode: (viewMode) => set({ viewMode }),
+
+ setSelectedNodeId: (selectedNodeId) => set({ selectedNodeId }),
+ setEditingNodeId: (editingNodeId) => set({ editingNodeId }),
+ setSelectedTaskId: (selectedTaskId) => set({ selectedTaskId }),
+ setSelectedPlanId: (selectedPlanId) => set({ selectedPlanId }),
+ setShowImportModal: (showImportModal) => set({ showImportModal }),
+ setCurrentUser: (currentUser) => {
+ if (currentUser) {
+ localStorage.setItem('quantum_user', JSON.stringify(currentUser));
+ } else {
+ localStorage.removeItem('quantum_user');
+ }
+ set({ currentUser });
+ },
+
+ logout: () => {
+ localStorage.removeItem('quantum_user');
+ set({
+ currentUser: null,
+ testPlans: [],
+ testTasks: [],
+ bugs: [],
+ spaceData: {}
+ });
+ },
+
+
+
+ addTestPlan: async (name, type, caseIds, assignees = []) => {
+ const planId = `p-${Date.now()}`;
+ const newPlan: TestPlan = {
+ id: planId,
+ name,
+ type,
+ caseIds: caseIds || [],
+ assignees: assignees,
+ createdAt: new Date().toISOString().split('T')[0]
+ };
+ // Optimistic update
+ set((state) => ({ testPlans: [...state.testPlans, newPlan] }));
+ const sid = get().currentSpaceId;
+ // Persist to DB
+ fetch(`http://localhost:8000/api/plans?space_id=${sid}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: planId, name, type, caseIds: caseIds || [], assignees, createdAt: newPlan.createdAt }),
+ }).catch(e => console.warn('[DB] addTestPlan failed', e));
+
+
+
+ const state = get();
+ await state.addTask({
+ name: `【执行任务】${name}`,
+ status: 'PENDING',
+ planId,
+ assignees: assignees,
+ });
+
+ // Ensure data is synced
+ await get().fetchTasks();
+ await get().fetchData();
+
+ },
+
+ updateTestPlan: async (id, updates) => {
+ set(state => ({
+ testPlans: state.testPlans.map(p => p.id === id ? { ...p, ...updates } : p)
+ }));
+ await fetch(`http://localhost:8000/api/plans/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updates),
+ });
+ // Refresh to get full sync
+ get().fetchTasks();
+ },
+
+ copyTestPlan: async (id) => {
+ const res = await fetch(`http://localhost:8000/api/plans/${id}/copy`, { method: 'POST' });
+ const data = await res.json();
+ if (data.id) {
+ // Refresh lists
+ get().fetchSpaces();
+ get().fetchTasks();
+ }
+ },
+
+ deleteTestPlan: async (id) => {
+
+ // Optimistic delete
+ set((state) => ({
+ testPlans: state.testPlans.filter(p => p.id !== id),
+ testTasks: state.testTasks.filter(t => t.planId !== id)
+ }));
+ // Persist to DB
+ fetch(`http://localhost:8000/api/plans/${id}`, { method: 'DELETE' })
+ .catch(e => console.warn('[DB] deleteTestPlan failed', e));
+ },
+
+
+ updateNodeSteps: (id, steps) => {
+ set((state) => {
+ const sid = state.currentSpaceId;
+ const currentTree = state.spaceData[sid];
+ const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] =>
+ nodes.map((node) => {
+ if (node.id === id) return { ...node, steps };
+ if (node.children) return { ...node, children: updateRecursive(node.children) };
+ return node;
+ });
+ return { spaceData: { ...state.spaceData, [sid]: updateRecursive(currentTree) } };
+ });
+ // Persist to DB
+ fetch(`http://localhost:8000/api/cases/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id, steps }),
+ }).catch(e => console.warn('[DB] updateNodeSteps failed', e));
+ },
+
+ updateNode: (id, updates) => {
+ // 1. Optimistic update
+ set((state) => {
+ const sid = state.currentSpaceId;
+ const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] =>
+ nodes.map((node) =>
+ node.id === id
+ ? { ...node, ...updates }
+ : { ...node, children: node.children ? updateRecursive(node.children) : [] }
+ );
+ return { spaceData: { ...state.spaceData, [sid]: updateRecursive(state.spaceData[sid] || []) } };
+ });
+ // 2. Persist to DB
+ fetch(`http://localhost:8000/api/cases/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id, ...updates }),
+ }).catch(e => console.warn('[DB] updateNode failed', e));
+ },
+
+ addNode: (parentId, title = '') => {
+ const sid = get().currentSpaceId;
+ const currentTree = get().spaceData[sid] || [];
+ const newNodeId = `node-${Date.now()}`;
+
+ let inheritedPriority: Priority | undefined;
+
+ let nextTree: TestCaseNode[];
+ if (!parentId) {
+ nextTree = [...currentTree, { id: newNodeId, text: title, priority: 'P2' }];
+ } else {
+ const traverseAndAdd = (nodes: TestCaseNode[]): TestCaseNode[] =>
+ nodes.map((node) => {
+ if (node.id === parentId) {
+ inheritedPriority = node.priority;
+ return {
+ ...node,
+ children: [...(node.children || []), { id: newNodeId, text: title, priority: node.priority || 'P2' }]
+ };
+ }
+ if (node.children) return { ...node, children: traverseAndAdd(node.children) };
+ return node;
+ });
+ nextTree = traverseAndAdd(currentTree);
+ }
+
+ set({ spaceData: { ...get().spaceData, [sid]: nextTree }, selectedNodeId: newNodeId, editingNodeId: newNodeId });
+
+ // Persist to DB
+ fetch(`http://localhost:8000/api/cases?space_id=${sid}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: newNodeId,
+ text: title,
+ parentId,
+ priority: inheritedPriority || 'P2'
+ }),
+ }).catch(e => console.warn('[DB] addNode failed', e));
+ },
+
+
+ addSiblingNode: (id, title = '') => {
+ const sid = get().currentSpaceId;
+ const currentTree = get().spaceData[sid];
+ const newNodeId = `node-${Date.now()}`;
+ let inheritedPriority: Priority | undefined;
+
+ const traverseAndAddSibling = (nodes: TestCaseNode[]): TestCaseNode[] => {
+ const index = nodes.findIndex((n) => n.id === id);
+ if (index !== -1) {
+ inheritedPriority = nodes[index].priority;
+ const newNodes = [...nodes];
+ newNodes.splice(index + 1, 0, { id: newNodeId, text: title, priority: inheritedPriority || 'P2' });
+ return newNodes;
+ }
+ return nodes.map((node) => {
+ if (node.children) {
+ const result = traverseAndAddSibling(node.children);
+ if (result !== node.children) return { ...node, children: result };
+ }
+ return node;
+ });
+ };
+ const nextTree = traverseAndAddSibling(currentTree);
+ set({ spaceData: { ...get().spaceData, [sid]: nextTree }, selectedNodeId: newNodeId, editingNodeId: newNodeId });
+
+ // Persist to DB
+ fetch(`http://localhost:8000/api/cases?space_id=${sid}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: newNodeId,
+ text: title,
+ priority: inheritedPriority || 'P2'
+ }),
+ }).catch(e => console.warn('[DB] addSiblingNode failed', e));
+ },
+
+
+ deleteNode: (id) => {
+ // Optimistic delete
+ set((state) => {
+ const sid = state.currentSpaceId;
+ const deleteRecursive = (nodes: TestCaseNode[]): TestCaseNode[] =>
+ nodes.filter((n) => n.id !== id).map((node) => ({
+ ...node,
+ children: node.children ? deleteRecursive(node.children) : [],
+ }));
+ return {
+ spaceData: { ...state.spaceData, [sid]: deleteRecursive(state.spaceData[sid] || []) },
+ selectedNodeId: state.selectedNodeId === id ? null : state.selectedNodeId,
+ };
+ });
+ // Persist to DB (cascades on backend)
+ fetch(`http://localhost:8000/api/cases/${id}`, { method: 'DELETE' })
+ .catch(e => console.warn('[DB] deleteNode failed', e));
+ },
+
+ addBug: (caseId, title) => {
+ const newBug: Bug = {
+ id: `BUG-${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`,
+ title,
+ status: 'OPEN',
+ caseId,
+ };
+ set((state) => {
+ const sid = state.currentSpaceId;
+ const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] =>
+ nodes.map((node) => {
+ if (node.id === caseId || node.caseId === caseId) return { ...node, bugId: newBug.id };
+ if (node.children) return { ...node, children: updateRecursive(node.children) };
+ return node;
+ });
+ return {
+ bugs: [...state.bugs, newBug],
+ spaceData: { ...state.spaceData, [sid]: updateRecursive(state.spaceData[sid] || []) },
+ };
+ });
+ // Persist bug to DB
+ fetch('http://localhost:8000/api/bugs', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: newBug.id, title, status: 'OPEN', caseId }),
+ }).catch(e => console.warn('[DB] addBug failed', e));
+ },
+
+ deleteBug: async (id) => {
+ // Optimistic delete
+ set((state) => ({
+ bugs: state.bugs.filter(b => b.id !== id)
+ }));
+ // Persist to DB
+ fetch(`http://localhost:8000/api/bugs/${id}`, { method: 'DELETE' })
+ .catch(e => console.warn('[DB] deleteBug failed', e));
+ },
+
+
+ getSelectedNode: () => {
+ const state = get();
+ if (!state.selectedNodeId) return null;
+ const currentTree = state.spaceData[state.currentSpaceId] || [];
+
+ const findRecursive = (nodes: TestCaseNode[]): TestCaseNode | null => {
+ for (const node of nodes) {
+ if (node.id === state.selectedNodeId) return node;
+ if (node.children) {
+ const found = findRecursive(node.children);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+ return findRecursive(currentTree);
+ },
+ importNodes: async (nodes) => {
+
+ const sid = get().currentSpaceId;
+ if (!sid) return;
+
+ // Flatten the tree for backend (backend expects flat list with parentId)
+ const flatNodes: any[] = [];
+ const flatten = (items: any[], pid: string | null = null) => {
+ items.forEach(item => {
+ flatNodes.push({
+ id: item.id,
+ text: item.text,
+ parentId: pid,
+ steps: item.steps || [],
+ tags: item.tags || []
+ });
+ if (item.children && item.children.length > 0) {
+ flatten(item.children, item.id);
+ }
+ });
+ };
+ flatten(nodes);
+
+ try {
+ await fetch(`http://localhost:8000/api/cases/batch?space_id=${sid}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(flatNodes)
+ });
+ // Refresh local data after import
+ await get().fetchData();
+ } catch (e) {
+ console.error('Failed to batch import nodes', e);
+ }
+ },
+
+ batchUpdateNodes: async (ids, updates) => {
+ // 1. Optimistic update
+ set((state) => {
+ const sid = state.currentSpaceId;
+ const updateRecursive = (nodes: TestCaseNode[]): TestCaseNode[] =>
+ nodes.map((node) => {
+ const newNode = ids.includes(node.id) ? { ...node, ...updates } : node;
+ if (newNode.children) {
+ newNode.children = updateRecursive(newNode.children);
+ }
+ return newNode;
+ });
+ return { spaceData: { ...state.spaceData, [sid]: updateRecursive(state.spaceData[sid] || []) } };
+ });
+
+ // 2. Persist to DB
+ try {
+ await fetch('http://localhost:8000/api/cases/batch', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ caseIds: ids,
+ maintainer: updates.maintainer,
+ reviewers: updates.reviewers
+ })
+ });
+ } catch (e) {
+ console.error('Failed to batch update nodes', e);
+ }
+ },
+}));
+
+
+
diff --git a/src/user.json b/src/user.json
new file mode 100644
index 0000000..327be28
--- /dev/null
+++ b/src/user.json
@@ -0,0 +1,27 @@
+{
+ "陶航宇": "ou_530d280ca8119a99fba2c0b31d5135ee",
+ "祖立峰": "ou_90b9d4c9645363f502ca5a825ecd7e83",
+ "付程玲": "ou_9334a6c5aab73d32419e1059763cbf26",
+ "王伟东": "ou_a8be7e4fbb78b8181ddfb6c883448709",
+ "孙悦": "ou_dafde22c2bc376d6440a39c819cf877c",
+ "侯志勇": "ou_f2cc58ae8ae5c43e59243a7fc976c25f",
+ "杨磊": "ou_7dd26678a97b59a082340e759ca569a9",
+ "王斌": "ou_798bd0a50d8c5f3eeb2924ec0bd65acf",
+ "刘亚琼": "ou_2351be9c2c6e465f37582f4cef5c32f4",
+ "杜凯": "ou_545c99a3dca03b175af61a30b124bfe9",
+ "路强": "ou_95187be353a3ccdfafa7061b0e42c411",
+ "张家浩": "ou_746a541759dc523290ef7062d42ae533",
+ "赵晗": "ou_812d3705b105ea342000d0dc4376a146",
+ "陈适": "ou_3b9699365ed8d28ba7f577f75a291644",
+ "杨祎伟": "ou_3587ec1084d6fac875e4e0d1fc0e9715",
+ "辛海丹": "ou_700f829a3de5af1685f6f3d90a9e200a",
+ "圣正杰": "ou_4ef3536f42ee674221b4cb9ffa469d4b",
+ "汪明": "ou_dccc3e3296ec78ef16a10d5a07e45799",
+ "李莹": "ou_e1c0c73e10d0a5a2f20e3ae853d5eb13",
+ "周天如": "ou_66bdd9ce844efaed6bd71a92325cfbb0",
+ "吴龙涛": "ou_6696b998749b8456729e4cfa4b295559",
+ "吴超": "ou_675dea340907ecb416f2c9d96f634192",
+ "郝建新": "ou_f9681314b63b485bf3e8650f7c43a411",
+ "董红帅": "ou_775ff733bd2d407ab21b7a9c4fa7fd3b",
+ "陈钦洋": "ou_6f1269a7e024a1711cfedc5d96052351"
+}
\ No newline at end of file
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..7f42e5f
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..d3c52ea
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "module": "esnext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..8b0f57b
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})