From 472234035dfd5baa5ea587f2f074802f39354240 Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Thu, 23 Apr 2026 09:41:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=A1=86=E6=9E=B6=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 +- MEMORY.md | 110 +- README.md | 11 +- api/app/api/project_requirement.py | 163 + api/app/api/router.py | 56 +- api/app/api/v1/calendar.py | 107 + api/app/api/v1/diary.py | 83 + api/app/api/v1/mermaids.py | 148 + api/app/api/v1/mind_map.py | 108 + api/app/api/v1/todos.py | 37 +- api/app/core/config.py | 29 +- api/app/core/database.py | 38 +- api/app/models/__init__.py | 7 +- api/app/models/audit_log.py | 2 +- api/app/models/auth_session.py | 2 +- api/app/models/calendar_event.py | 42 + api/app/models/chat.py | 4 +- api/app/models/diary.py | 41 + api/app/models/file_storage.py | 2 +- api/app/models/hot_search.py | 8 +- api/app/models/life_countdown.py | 2 +- api/app/models/mermaid_diagram.py | 56 + api/app/models/mind_map.py | 35 + api/app/models/model_registry.py | 4 +- api/app/models/object_group.py | 57 + api/app/models/question_bank.py | 4 +- api/app/models/rbac.py | 2 +- api/app/models/requirement.py | 110 +- api/app/models/system_message.py | 4 +- api/app/models/system_param.py | 4 +- api/app/models/todo.py | 61 +- api/app/models/user.py | 25 +- api/app/models/vocabulary_word.py | 4 +- api/app/schemas/calendar_event.py | 75 + api/app/schemas/diary.py | 58 + api/app/schemas/mermaid.py | 85 + api/app/schemas/mind_map.py | 46 + api/app/schemas/requirement.py | 10 +- api/app/schemas/todo.py | 59 +- api/app/services/calendar_event_service.py | 602 ++++ api/app/services/diary_service.py | 186 ++ api/app/services/mermaid_service.py | 525 ++++ api/app/services/mind_map_service.py | 373 +++ api/app/services/requirement_service.py | 685 +++- api/app/services/seed_service.py | 4 +- api/app/services/todo_service.py | 237 +- docker-compose.yml | 13 +- memory/2026-04-19.md | 34 + memory/2026-04-22.md | 320 ++ memory/2026-04-23.md | 58 + package-lock.json | 2672 ++++++---------- web/Dockerfile | 1 + web/package-lock.json | 2760 ++++++----------- web/package.json | 4 +- web/src/app/admin/chat/page.tsx | 2 +- web/src/app/admin/diary/page.tsx | 611 +++- web/src/app/admin/files/page.tsx | 2 +- web/src/app/admin/hot-search/page.tsx | 2 +- web/src/app/admin/jwt-generator/page.tsx | 2 +- web/src/app/admin/layout.tsx | 87 +- web/src/app/admin/life-countdown/page.tsx | 2 +- web/src/app/admin/mdresolve/page.tsx | 2 +- web/src/app/admin/menus/page.tsx | 72 +- web/src/app/admin/mermaid-mgr/[id]/page.tsx | 10 + .../_components/mermaid-editor.tsx | 391 +++ web/src/app/admin/mermaid-mgr/page.tsx | 532 +++- .../mindmap/_components/mindmap-editor.tsx | 589 ++++ web/src/app/admin/mindmap/edit/[id]/page.tsx | 10 + web/src/app/admin/mindmap/edit/page.tsx | 5 + web/src/app/admin/mindmap/page.tsx | 840 ++--- web/src/app/admin/models/page.tsx | 2 +- web/src/app/admin/page.tsx | 10 +- web/src/app/admin/password/page.tsx | 2 +- web/src/app/admin/question-bank/page.tsx | 585 +++- web/src/app/admin/requirements/[id]/page.tsx | 16 +- web/src/app/admin/requirements/new/page.tsx | 6 +- web/src/app/admin/requirements/page.tsx | 139 +- web/src/app/admin/roles/page.tsx | 2 +- web/src/app/admin/schedule/page.tsx | 876 +++++- web/src/app/admin/syslog/page.tsx | 2 +- web/src/app/admin/system-message/page.tsx | 2 +- web/src/app/admin/system-params/page.tsx | 2 +- web/src/app/admin/tag/page.tsx | 2 +- web/src/app/admin/todos/page.tsx | 935 +++--- web/src/app/admin/token-usage/page.tsx | 2 +- web/src/app/admin/users/page.tsx | 105 +- .../app/admin/vocabulary-proficiency/page.tsx | 2 +- web/src/app/admin/vocabulary/page.tsx | 2 +- web/src/app/layout.tsx | 4 +- web/src/app/page.tsx | 537 +++- web/src/components/mermaid-viewer.tsx | 60 + web/src/components/row-action-menu.tsx | 56 + web/src/components/ui-antd.tsx | 1005 ++++++ web/src/types/antd.d.ts | 1 + web/src/types/auth.ts | 116 +- 95 files changed, 12712 insertions(+), 5099 deletions(-) create mode 100644 api/app/api/project_requirement.py create mode 100644 api/app/api/v1/calendar.py create mode 100644 api/app/api/v1/diary.py create mode 100644 api/app/api/v1/mermaids.py create mode 100644 api/app/api/v1/mind_map.py create mode 100644 api/app/models/calendar_event.py create mode 100644 api/app/models/diary.py create mode 100644 api/app/models/mermaid_diagram.py create mode 100644 api/app/models/mind_map.py create mode 100644 api/app/models/object_group.py create mode 100644 api/app/schemas/calendar_event.py create mode 100644 api/app/schemas/diary.py create mode 100644 api/app/schemas/mermaid.py create mode 100644 api/app/schemas/mind_map.py create mode 100644 api/app/services/calendar_event_service.py create mode 100644 api/app/services/diary_service.py create mode 100644 api/app/services/mermaid_service.py create mode 100644 api/app/services/mind_map_service.py create mode 100644 memory/2026-04-22.md create mode 100644 memory/2026-04-23.md create mode 100644 web/src/app/admin/mermaid-mgr/[id]/page.tsx create mode 100644 web/src/app/admin/mermaid-mgr/_components/mermaid-editor.tsx create mode 100644 web/src/app/admin/mindmap/_components/mindmap-editor.tsx create mode 100644 web/src/app/admin/mindmap/edit/[id]/page.tsx create mode 100644 web/src/app/admin/mindmap/edit/page.tsx create mode 100644 web/src/components/mermaid-viewer.tsx create mode 100644 web/src/components/row-action-menu.tsx create mode 100644 web/src/components/ui-antd.tsx create mode 100644 web/src/types/antd.d.ts diff --git a/.env.example b/.env.example index 899c706..6494931 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,13 @@ API_HOST=0.0.0.0 API_PORT=8000 API_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 API_CORS_ORIGIN_REGEX= -DATABASE_URL=postgresql+psycopg://fquiz:fquiz@db:5432/fquiz +DATABASE_URL= +DB_HOST=223.109.142.84 +DB_PORT=5432 +DB_NAME=postgres +DB_SCHEMA=public +DB_USERNAME=postgres +DB_PASSWORD=1qazZAQ! FILE_VFS_ROOT=./data/vfs JWT_SECRET_KEY=change-this-in-production ACCESS_TOKEN_EXPIRE_MINUTES=15 diff --git a/MEMORY.md b/MEMORY.md index fb39f7a..6d80978 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -109,6 +109,12 @@ - 站点品牌标题统一使用 `Quiz`:至少保持 `web/src/app/layout.tsx` 的 `metadata.title` 与首页主标题一致。 - 登录页默认不展示 `API Base URL`,仅保留 `getApiBaseUrl()` 在请求链路中的能力(不影响鉴权与 API 调用逻辑)。 +## 登录页动效口径(2026-04-22) + +- 登录页主视觉允许使用装饰性动效(如浮动背景与角色动画),但必须保持登录/注册接口调用链路与鉴权行为不变。 +- 首页怪兽交互基线:眼睛跟随鼠标,密码输入框聚焦时主动挪开视线(避免“盯着密码输入”观感)。 +- 当前实现位于 `web/src/app/page.tsx`;若后续继续扩展动效,优先抽离样式与展示组件,避免登录业务与视觉代码耦合过深。 + ## AI 聊天口径(2026-04-13) - 一期聊天入口固定为后台路由 `/admin/chat`,权限码为 `chat.use`。 @@ -140,7 +146,7 @@ ## 菜单迁移口径(2026-04-18) -- `日程管理` 菜单迁移采用最小改动策略:新增菜单 `admin.schedule`(`/admin/schedule`,权限 `todo.read`),并直接复用 `todos` 页面能力作为日程管理承载。 +- `日程管理` 菜单已升级为独立日程模块:保留菜单 `admin.schedule`(`/admin/schedule`,权限 `todo.read`),前端由 `web/src/app/admin/schedule/page.tsx` 承载年/月/周视图、编辑、完成、AI 生成交互,不再复用 `todos` 页面。 - `admin.schedule` 已加入后端与前端受保护菜单集合,避免在菜单管理中被误删。 - `家庭作业` 菜单迁移采用最小改动策略:新增菜单 `admin.homework`(`/admin/homework`,权限 `question_bank.read`),并直接复用 `question-bank` 页面能力作为家庭作业承载。 - `admin.homework` 已加入后端与前端受保护菜单集合,避免在菜单管理中被误删。 @@ -152,7 +158,7 @@ - `价格监控` 菜单迁移沿用 Token 统计能力:保留菜单编码 `admin.token_usage` 与权限 `model.read`,菜单文案统一为“价格监控”,默认路由为 `/admin/price-monitor`,并由 `web/src/app/admin/price-monitor/page.tsx` 复用 `token-usage` 页面实现。 - `API测试` 菜单迁移沿用模型测试能力:新增菜单编码 `admin.api_tester`(`/admin/api-tester`,权限 `model.read`),由 `web/src/app/admin/api-tester/page.tsx` 复用 `models` 页面承载“冒烟测试/对话测试/测试记录”能力;并已加入后端与前端受保护菜单集合、admin 默认菜单绑定与后台首页入口。 - `模型管理` 菜单迁移沿用既有模型能力:保留菜单编码 `admin.models`(`/admin/models`,权限 `model.read/model.manage`),继续由 `web/src/app/admin/models/page.tsx` 承载模型台账、状态流转、路由规则、密钥轮换、健康检查与测试能力;并已加入后端与前端受保护菜单集合、admin 默认菜单绑定与后台首页入口。 -- `流程图` 菜单迁移沿用 MD 解析能力:新增菜单编码 `admin.mermaid_mgr`(`/admin/mermaid-mgr`,权限 `question_bank.read`),由 `web/src/app/admin/mermaid-mgr/page.tsx` 复用 `mdresolve` 页面承载 Markdown 解析与批量导入能力;并已加入后端与前端受保护菜单集合、admin 默认菜单绑定与后台首页入口。 +- `流程图` 菜单已切回独立 Mermaid 管理能力:菜单编码 `admin.mermaid_mgr`(`/admin/mermaid-mgr`,权限 `question_bank.read`),前端由 `web/src/app/admin/mermaid-mgr/page.tsx` 承载列表/分组/新建编辑删除,编辑页为 `web/src/app/admin/mermaid-mgr/[id]/page.tsx`(AI 流式改图 + 预览 + 保存);后端接口对齐 quiz 路径:`/api/v1/mermaids/diagrams/*`,并兼容 `/api/mermaids/diagrams/*` legacy 前缀。 - `上帝视角` 菜单迁移沿用系统日志能力:新增菜单编码 `admin.diary`(`/admin/diary`,权限 `menu.read`),由 `web/src/app/admin/diary/page.tsx` 复用 `syslog` 页面承载审计日志查询能力;并已加入后端与前端受保护菜单集合、admin 默认菜单绑定与后台首页入口。 - `待办管理` 菜单迁移采用最小改动策略:保留菜单编码 `admin.todos`(`/admin/todos`,权限 `todo.read`),并沿用现有 `todos` 页面能力承载待办管理完整交互(筛选、创建、状态流转、删除)。 - `队列管理` 菜单迁移采用最小改动策略:新增菜单编码 `admin.queue_mgr`(`/admin/jobqueue`,权限 `todo.read`),并由 `web/src/app/admin/jobqueue/page.tsx` 复用 `todos` 页面能力承接队列任务清单管理;已加入后端与前端受保护菜单集合、admin 默认菜单绑定与后台首页入口。 @@ -178,3 +184,103 @@ - `web/src/app/layout.tsx` 只负责注入 `@radix-ui/themes/styles.css` 与 `Theme` Provider,不再通过根容器类叠加自定义主题视觉。 - `web/src/app/admin/layout.tsx` 使用 Radix Themes 组件(`Card/Flex/Text/Heading/Button/Callout`)组织后台壳层,避免硬编码品牌色光斑与渐变块。 - `web/src/app/**` 中 `Button` 视觉优先通过 `variant / color / size` 控制;不再使用长 Tailwind 颜色类拼接按钮主题。 + +## 前端菜单交互口径(2026-04-19) + +- 后台壳层(`web/src/app/admin/layout.tsx`)已采用 `@radix-ui/themes` 的 `DropdownMenu` 承接菜单交互: + - 移动端(`md` 以下)菜单入口统一为“菜单”下拉,不再直接渲染左侧长列表; + - 顶部账号区“返回首页/退出登录”统一收口到“账号”下拉。 +- 后台表格行内“操作”入口推荐统一为下拉菜单形态,优先复用 `web/src/components/row-action-menu.tsx`,避免页面内重复堆叠小按钮并降低操作列宽度波动。 +- Phase B 样板页已落地:`/admin/users`、`/admin/requirements`、`/admin/menus`;后续页面迁移默认保持“业务逻辑不动,仅替换操作入口承载组件”的最小改动策略。 + +## 数据库连接口径(2026-04-22) + +- API 默认数据库连接改为外部 PostgreSQL:优先读取 `DATABASE_URL`;若未设置则由 `DB_HOST/DB_PORT/DB_NAME/DB_USERNAME/DB_PASSWORD` 组装。 +- `DB_SCHEMA` 通过 PostgreSQL `search_path` 注入,语义等价 JDBC 的 `currentSchema`。 +- `docker compose` 中本地 `db` 服务改为 `local-db` profile(默认不启动);仅在需要本地库时显式 `docker compose --profile local-db up -d`。 +- API 启动初始化口径:`seed_defaults` 仅对本地数据库目标执行(`db/localhost/127.0.0.1/::1` 或 `DATABASE_URL` 指向本地);非本地目标跳过默认数据写入,避免对外部库做不兼容初始化。 +- 用户表兼容口径:用户主键列对齐旧库 `users.user_id`,与用户关联的外键统一引用 `users.user_id`(不再引用 `users.id`)。 + +## 前端组件栈口径(2026-04-22) + +- 组件库基线从 `Radix UI` 切换为 `Ant Design`,`web` 依赖已移除 `@radix-ui/themes` / `@radix-ui/react-dialog` / `@radix-ui/react-select`,新增 `antd`。 +- 为控制迁移范围,新增兼容层 `web/src/components/ui-antd.tsx`:对外保持 `Button/Card/Flex/Text/Heading/TextField/TextArea/Select/Dialog/DropdownMenu/Callout/Table/Checkbox/Theme` API 形态,内部使用 AntD 实现。 +- `web/src/app/layout.tsx` 统一注入 `antd/dist/reset.css`,并通过兼容层 `Theme` 提供全局主题 token。 +- 工程约束更新: + - 页面/组件禁止继续新增 `@radix-ui/themes` 导入; + - 优先从 `@/components/ui-antd` 引入 UI 组件; + - 新增页面如需 AntD 高级能力,可直接引入 `antd`,但需保持与现有主题和交互风格一致。 +- 兼容说明:`web/src/types/antd.d.ts` 仅保留 `antd/dist/reset.css` 声明,禁止再写 `declare module "antd"`;否则会覆盖官方类型并导致 `Form.useForm` 等泛型调用在 `next build` 的 TypeScript 阶段失败。 +- `web/src/components/ui-antd.tsx` 作为兼容层时,若自定义 `type/variant/size/checked` 等语义,必须先 `Omit` 掉对应 AntD 原生同名字段再重定义,否则会触发联合类型冲突并阻断 Docker 构建。 + +## 需求管理兼容口径(2026-04-22) + +- 需求管理底层表结构已切换为老工程口径: + - 主表:`project_requirement` + - 生命周期表:`project_requirement_log` +- `fquiz` 需求模块保持“双接口并行”策略: + - 现有前端接口:`/api/v1/requirements*` + - 老工程兼容接口:`/api/project/requirement/*`(`search/get/status/analyze/review/lifecycle/history-options/pending`) + +## 思维导图兼容口径(2026-04-22) + +- 思维导图已从“题库统计复用页”切回独立模块,后端主表固定为老工程口径 `mind_map`。 +- API 入口统一采用老工程风格路径(挂在 `/api/v1` 下): + - `POST /api/v1/mindmap/search` + - `GET /api/v1/mindmap/get/{id}` + - `POST /api/v1/mindmap/create` + - `PUT /api/v1/mindmap/update-basic-info` + - `PUT /api/v1/mindmap/update-data` + - `DELETE /api/v1/mindmap/delete/{id}` + - `GET /api/v1/mindmap/generate/stream` +- Todo 分析链路已恢复老逻辑:`POST /api/v1/todos/{todo_id}/init-mindmap` 会真实创建/复用 `mind_map(id=todo_id)` 并返回导图信息,前端跳转到 `/admin/mindmap/edit/{id}`。 +- 当前前端编辑器基线为“JSON 编辑 + 树预览 + AI 流式生成 + JSON/Markdown 导出”;如需老工程 `mind-elixir` 的可视化拖拽编辑,需单独引入并适配 AntD/Next 页面栈。 +- 状态机口径对齐老工程:`PENDING_ANALYSIS -> PENDING_REVIEW/PENDING_REVISION/OPEN -> IN_PROGRESS -> COMPLETED/CLOSED`;并兼容映射 `CANCELLED -> CLOSED`。 +- 优先级口径对齐老工程存储:数据库落库使用大写 `LOW/MEDIUM/HIGH`;API 层兼容小写输入并向前端返回小写展示值。 +- 旧表不包含 `assignee/reviewer/due_at` 等字段,`/api/v1/requirements` 中这些字段当前作为兼容占位返回,后续如需恢复需补扩展表或业务映射策略。 +- 老工程兼容接口补充 `POST /api/project/requirement/{id}/design`,用于需求设计阶段回写(`PENDING_ANALYSIS` 内部闭环)。 + +## 待办管理兼容口径(2026-04-22) + +- 待办模块已切换到 quiz 表口径:`api/app/models/todo.py` 使用 `todo` 表(非 `todos`),字段为 `title/descr/status/priority/start_time/due_date/expire_time/calendar_event_id/create_date/create_user/update_date/update_user`。 +- 状态机与优先级固定为: + - 状态:`SCHEDULED/IN_PROGRESS/COMPLETED/CANCELLED/EXPIRED` + - 优先级:`LOW/MEDIUM/HIGH` +- `/api/v1/todos` 查询口径对齐 quiz:默认按当前登录用户 `create_user` 过滤,仅返回本人待办;支持 `title/status/priority/page_num/page_size`。 +- 待办扩展接口口径: + - `POST /api/v1/todos/{todo_id}/complete`:完成待办(置 `COMPLETED`) + - `POST /api/v1/todos/{todo_id}/init-mindmap`:创建或复用 `mind_map(id=todo_id)` 并返回导图详情 +- 前端 `web/src/app/admin/todos/page.tsx` 已按 quiz 交互重构:默认状态筛选 `SCHEDULED`、分页列表、新增/编辑/详情、分析、完成、删除;`jobqueue/cron` 继续复用该页面。 + +## 日程管理兼容口径(2026-04-22) + +- 日程模块已切换到 quiz 表口径:`api/app/models/calendar_event.py` 使用 `calendar_event` 表,字段为 `title/descr/status/priority/start_time/end_time/expire_time/all_day/completed_at/todo_id/create_date/create_user/update_date/update_user`。 +- 日程 API 固定为: + - `POST /api/v1/calendar/search` + - `GET /api/v1/calendar/get/{id}` + - `POST /api/v1/calendar/create` + - `PUT /api/v1/calendar/update` + - `DELETE /api/v1/calendar/delete/{id}` + - `POST /api/v1/calendar/{id}/complete` + - `GET /api/v1/calendar/generate/stream` +- 日程与待办保持双向同步: + - 日程创建/更新/删除/完成会同步到 `todo` +- 待办创建/更新/删除/状态流转会同步到 `calendar_event` +- 通过 `is_sync/syncing` 标记防止递归回环。 + +## 日记管理兼容口径(2026-04-23) + +- `admin.diary` 已从系统日志复用页切换为独立 Diary 模块,后端主表固定为老工程口径 `diary`。 +- Diary 表字段口径:`id/title/content/diary_date/mood/weather/archived/create_date/create_user/update_date/update_user`,并保留 `create_user` 维度隔离查询。 +- API 入口统一采用老工程风格路径(挂在 `/api/v1` 下): + - `POST /api/v1/diary/search` + - `GET /api/v1/diary/get/{id}` + - `POST /api/v1/diary/create` + - `PUT /api/v1/diary/update` + - `DELETE /api/v1/diary/delete/{id}` + - `POST /api/v1/diary/{id}/archive?archived=...` +- 查询逻辑对齐老工程:支持 `title/mood/diary_date_start/diary_date_end/archived` 过滤,排序 `diary_date DESC, create_date DESC`。 +- 当前权限沿用兼容口径: + - 读:`menu.read | menu.manage` + - 写:`menu.manage` + 后续若需细粒度可拆分 `diary.read/diary.manage`。 diff --git a/README.md b/README.md index 0104166..ccbcc11 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,12 @@ docker compose up --build -d ``` + 如需同时启动本地 PostgreSQL 容器(`db`),使用: + + ```bash + docker compose --profile local-db up --build -d + ``` + 3. 查看运行状态和日志: ```bash @@ -109,7 +115,8 @@ - 前端:`http://localhost:3000` - 后端:`http://localhost:8000/health` -- PostgreSQL:`localhost:5433`(可通过 `POSTGRES_PORT` 覆盖) +- PostgreSQL:默认连接外部库(`DB_HOST/DB_PORT/DB_NAME/DB_SCHEMA/DB_USERNAME/DB_PASSWORD`) +- 本地 PostgreSQL(可选):启用 `local-db` profile 后使用 `localhost:5433`(可通过 `POSTGRES_PORT` 覆盖) 5. 停止并清理: @@ -119,7 +126,7 @@ 说明: - `NEXT_PUBLIC_API_BASE_URL` 在 Next.js 中是构建期注入;如果修改该值,需要重新执行 `docker compose up --build`。 -- 若使用 Docker Compose,默认 `DATABASE_URL` 指向容器内 `db` 服务(PostgreSQL)。 +- 若未显式设置 `DATABASE_URL`,API 会使用 `DB_*` 变量自动组装 PostgreSQL 连接,并将 `DB_SCHEMA` 作为 `search_path`(等价 JDBC 的 `currentSchema` 语义)。 - 若出现跨域(CORS)错误,请在 `.env` 配置: - `API_CORS_ORIGINS`:精确来源列表(逗号分隔),如 `https://admin.example.com,http://localhost:3000` - `API_CORS_ORIGIN_REGEX`:来源正则(可选),如 `https://.*\\.example\\.com` diff --git a/api/app/api/project_requirement.py b/api/app/api/project_requirement.py new file mode 100644 index 0000000..9ca245e --- /dev/null +++ b/api/app/api/project_requirement.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from typing import Literal + +from fastapi import APIRouter, Depends, Query +from pydantic import BaseModel, Field +from sqlalchemy.orm import Session + +from ..core.database import get_db +from ..core.dependencies import CurrentUser, require_any_permission, require_permission +from ..schemas.auth import MessageResponse +from ..services.requirement_service import ( + analyze_requirement_legacy, + design_requirement_legacy, + get_history_options_legacy, + get_pending_requirement_legacy, + get_requirement_legacy, + list_lifecycle_legacy, + review_requirement_legacy, + search_requirements_legacy, + update_status_legacy, +) + +router = APIRouter(prefix="/api/project/requirement", tags=["project-requirement"]) + + +class RequirementSearchLegacyRequest(BaseModel): + title: str | None = None + projectName: str | None = None + status: str | None = None + priority: str | None = None + pageNum: int = Field(default=1, ge=1) + pageSize: int = Field(default=10, ge=1, le=500) + + +class RequirementAnalyzeLegacyRequest(BaseModel): + descr: str | None = None + progressPercent: int | None = Field(default=None, ge=0, le=100) + + +class RequirementReviewLegacyRequest(BaseModel): + decision: Literal["TO_REVISION", "TO_OPEN"] + descr: str | None = None + comment: str | None = None + + +@router.get("/pending") +def get_pending_requirement( + _: CurrentUser = Depends(require_permission("requirement.read")), + db: Session = Depends(get_db), +) -> dict | None: + return get_pending_requirement_legacy(db) + + +@router.post("/search") +def search_requirements( + payload: RequirementSearchLegacyRequest, + _: CurrentUser = Depends(require_permission("requirement.read")), + db: Session = Depends(get_db), +) -> dict: + return search_requirements_legacy( + db, + page_num=payload.pageNum, + page_size=payload.pageSize, + project_name=payload.projectName, + status_value=payload.status, + priority_value=payload.priority, + title=payload.title, + ) + + +@router.get("/get/{requirement_id}") +def get_requirement( + requirement_id: str, + _: CurrentUser = Depends(require_permission("requirement.read")), + db: Session = Depends(get_db), +) -> dict: + return get_requirement_legacy(db, requirement_id) + + +@router.post("/{requirement_id}/status", response_model=MessageResponse) +def update_requirement_status( + requirement_id: str, + status_value: str = Query(..., alias="status"), + result_msg: str | None = Query(default=None, alias="resultMsg"), + progress_percent: int | None = Query(default=None, alias="progressPercent"), + current_user: CurrentUser = Depends(require_any_permission("requirement.process", "requirement.manage")), + db: Session = Depends(get_db), +) -> MessageResponse: + update_status_legacy( + db, + requirement_id=requirement_id, + status_value=status_value, + result_msg=result_msg, + progress_percent=progress_percent, + actor_user_id=current_user.user.id, + ) + return MessageResponse(message="Requirement status updated") + + +@router.post("/{requirement_id}/analyze") +def analyze_requirement( + requirement_id: str, + payload: RequirementAnalyzeLegacyRequest, + current_user: CurrentUser = Depends(require_any_permission("requirement.process", "requirement.manage")), + db: Session = Depends(get_db), +) -> dict: + return analyze_requirement_legacy( + db, + requirement_id=requirement_id, + descr=payload.descr, + progress_percent=payload.progressPercent, + actor_user_id=current_user.user.id, + ) + + +@router.post("/{requirement_id}/design") +def design_requirement( + requirement_id: str, + payload: RequirementAnalyzeLegacyRequest, + current_user: CurrentUser = Depends(require_any_permission("requirement.process", "requirement.manage")), + db: Session = Depends(get_db), +) -> dict: + return design_requirement_legacy( + db, + requirement_id=requirement_id, + descr=payload.descr, + actor_user_id=current_user.user.id, + ) + + +@router.post("/{requirement_id}/review") +def review_requirement( + requirement_id: str, + payload: RequirementReviewLegacyRequest, + current_user: CurrentUser = Depends(require_any_permission("requirement.process", "requirement.manage")), + db: Session = Depends(get_db), +) -> dict: + return review_requirement_legacy( + db, + requirement_id=requirement_id, + decision=payload.decision, + descr=payload.descr, + comment=payload.comment, + actor_user_id=current_user.user.id, + ) + + +@router.get("/{requirement_id}/lifecycle") +def get_requirement_lifecycle( + requirement_id: str, + _: CurrentUser = Depends(require_permission("requirement.read")), + db: Session = Depends(get_db), +) -> list[dict]: + return list_lifecycle_legacy(db, requirement_id) + + +@router.get("/history-options") +def get_requirement_history_options( + _: CurrentUser = Depends(require_permission("requirement.read")), + db: Session = Depends(get_db), +) -> dict: + return get_history_options_legacy(db) diff --git a/api/app/api/router.py b/api/app/api/router.py index 84503fd..a3353e8 100644 --- a/api/app/api/router.py +++ b/api/app/api/router.py @@ -1,13 +1,18 @@ from fastapi import APIRouter +from .project_requirement import router as project_requirement_router from .v1.admin import router as admin_router from .v1.admin_files import router as admin_files_router from .v1.auth import router as auth_router +from .v1.calendar import router as calendar_router from .v1.chat import router as chat_router +from .v1.diary import router as diary_router from .v1.hot_search import router as hot_search_router from .v1.jwt_generator import router as jwt_generator_router from .v1.life_countdown import router as life_countdown_router from .v1.mdresolve import router as mdresolve_router +from .v1.mermaids import router as mermaids_router +from .v1.mind_map import router as mind_map_router from .v1.question_bank import router as question_bank_router from .v1.requirements import router as requirements_router from .v1.system_messages import router as system_messages_router @@ -18,26 +23,39 @@ from .v1.users import router as users_router from .v1.vocabulary import router as vocabulary_router from .v1.ws import router as ws_router -api_router = APIRouter(prefix="/api/v1") -api_router.include_router(auth_router) -api_router.include_router(users_router) -api_router.include_router(admin_router) -api_router.include_router(admin_files_router) -api_router.include_router(requirements_router) -api_router.include_router(todos_router) -api_router.include_router(token_usage_router) -api_router.include_router(system_messages_router) -api_router.include_router(system_params_router) -api_router.include_router(jwt_generator_router) -api_router.include_router(chat_router) -api_router.include_router(life_countdown_router) -api_router.include_router(question_bank_router) -api_router.include_router(hot_search_router) -api_router.include_router(mdresolve_router) -api_router.include_router(vocabulary_router) -api_router.include_router(ws_router) +v1_router = APIRouter(prefix="/api/v1") +v1_router.include_router(auth_router) +v1_router.include_router(users_router) +v1_router.include_router(admin_router) +v1_router.include_router(admin_files_router) +v1_router.include_router(requirements_router) +v1_router.include_router(todos_router) +v1_router.include_router(token_usage_router) +v1_router.include_router(system_messages_router) +v1_router.include_router(system_params_router) +v1_router.include_router(jwt_generator_router) +v1_router.include_router(chat_router) +v1_router.include_router(calendar_router) +v1_router.include_router(diary_router) +v1_router.include_router(life_countdown_router) +v1_router.include_router(question_bank_router) +v1_router.include_router(mind_map_router) +v1_router.include_router(hot_search_router) +v1_router.include_router(mdresolve_router) +v1_router.include_router(mermaids_router) +v1_router.include_router(vocabulary_router) +v1_router.include_router(ws_router) + +legacy_mermaid_router = APIRouter(prefix="/api") +legacy_mermaid_router.include_router(mermaids_router) -@api_router.get("/ping") +@v1_router.get("/ping") def ping() -> dict[str, str]: return {"message": "pong"} + + +api_router = APIRouter() +api_router.include_router(v1_router) +api_router.include_router(project_requirement_router) +api_router.include_router(legacy_mermaid_router) diff --git a/api/app/api/v1/calendar.py b/api/app/api/v1/calendar.py new file mode 100644 index 0000000..970b8cb --- /dev/null +++ b/api/app/api/v1/calendar.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session + +from ...core.database import get_db +from ...core.dependencies import CurrentUser, require_any_permission, require_permission +from ...schemas.calendar_event import ( + CalendarEventCreateRequest, + CalendarEventPageResponse, + CalendarEventQueryRequest, + CalendarEventSummary, + CalendarEventUpdateRequest, +) +from ...services.calendar_event_service import ( + complete_calendar_event, + create_calendar_event, + delete_calendar_event, + get_calendar_event_by_id, + search_calendar_events, + serialize_calendar_event, + stream_generate_calendar_event, + update_calendar_event, +) + +router = APIRouter(prefix="/calendar", tags=["calendar"]) + + +@router.post("/search", response_model=CalendarEventPageResponse) +def search_calendar_endpoint( + payload: CalendarEventQueryRequest, + current_user: CurrentUser = Depends(require_permission("todo.read")), + db: Session = Depends(get_db), +) -> CalendarEventPageResponse: + return search_calendar_events(db, payload, actor=current_user.user) + + +@router.get("/get/{event_id}", response_model=CalendarEventSummary) +def get_calendar_endpoint( + event_id: str, + current_user: CurrentUser = Depends(require_permission("todo.read")), + db: Session = Depends(get_db), +) -> CalendarEventSummary: + event = get_calendar_event_by_id(db, event_id, actor=current_user.user) + if not event: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Calendar event not found") + return serialize_calendar_event(event) + + +@router.post("/create", response_model=CalendarEventSummary) +def create_calendar_endpoint( + payload: CalendarEventCreateRequest, + current_user: CurrentUser = Depends(require_any_permission("todo.create", "todo.manage")), + db: Session = Depends(get_db), +) -> CalendarEventSummary: + return create_calendar_event(db, payload, actor=current_user.user) + + +@router.put("/update", response_model=CalendarEventSummary) +def update_calendar_endpoint( + payload: CalendarEventUpdateRequest, + current_user: CurrentUser = Depends(require_any_permission("todo.process", "todo.manage")), + db: Session = Depends(get_db), +) -> CalendarEventSummary: + return update_calendar_event(db, payload, actor=current_user.user) + + +@router.delete("/delete/{event_id}") +def delete_calendar_endpoint( + event_id: str, + current_user: CurrentUser = Depends(require_any_permission("todo.process", "todo.manage")), + db: Session = Depends(get_db), +) -> dict[str, bool]: + return delete_calendar_event(db, event_id, actor=current_user.user) + + +@router.post("/{event_id}/complete", response_model=CalendarEventSummary) +def complete_calendar_endpoint( + event_id: str, + current_user: CurrentUser = Depends(require_any_permission("todo.process", "todo.manage")), + db: Session = Depends(get_db), +) -> CalendarEventSummary: + return complete_calendar_event(db, event_id, actor=current_user.user) + + +@router.get("/generate/stream") +async def generate_calendar_stream_endpoint( + descr: str = Query(min_length=1), + _: CurrentUser = Depends(require_permission("todo.read")), + db: Session = Depends(get_db), +) -> StreamingResponse: + async def event_gen() -> AsyncGenerator[str, None]: + async for chunk in stream_generate_calendar_event(db, descr=descr): + yield f"data: {chunk}\n\n" + + return StreamingResponse( + event_gen(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) diff --git a/api/app/api/v1/diary.py b/api/app/api/v1/diary.py new file mode 100644 index 0000000..1cf6f23 --- /dev/null +++ b/api/app/api/v1/diary.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from ...core.database import get_db +from ...core.dependencies import CurrentUser, require_any_permission, require_permission +from ...schemas.diary import ( + DiaryCreateRequest, + DiaryPageResponse, + DiaryQueryRequest, + DiarySummary, + DiaryUpdateRequest, +) +from ...services.diary_service import ( + archive_diary, + create_diary, + delete_diary, + get_diary_by_id, + search_diaries, + serialize_diary, + update_diary, +) + +router = APIRouter(prefix="/diary", tags=["diary"]) + + +@router.post("/search", response_model=DiaryPageResponse) +def search_diary_endpoint( + payload: DiaryQueryRequest, + current_user: CurrentUser = Depends(require_any_permission("menu.read", "menu.manage")), + db: Session = Depends(get_db), +) -> DiaryPageResponse: + return search_diaries(db, payload, actor=current_user.user) + + +@router.get("/get/{diary_id}", response_model=DiarySummary) +def get_diary_endpoint( + diary_id: str, + current_user: CurrentUser = Depends(require_any_permission("menu.read", "menu.manage")), + db: Session = Depends(get_db), +) -> DiarySummary: + item = get_diary_by_id(db, diary_id, actor=current_user.user) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Diary not found") + return serialize_diary(item) + + +@router.post("/create", response_model=DiarySummary) +def create_diary_endpoint( + payload: DiaryCreateRequest, + current_user: CurrentUser = Depends(require_permission("menu.manage")), + db: Session = Depends(get_db), +) -> DiarySummary: + return create_diary(db, payload, actor=current_user.user) + + +@router.put("/update", response_model=DiarySummary) +def update_diary_endpoint( + payload: DiaryUpdateRequest, + current_user: CurrentUser = Depends(require_permission("menu.manage")), + db: Session = Depends(get_db), +) -> DiarySummary: + return update_diary(db, payload, actor=current_user.user) + + +@router.delete("/delete/{diary_id}") +def delete_diary_endpoint( + diary_id: str, + current_user: CurrentUser = Depends(require_permission("menu.manage")), + db: Session = Depends(get_db), +) -> dict[str, bool]: + return delete_diary(db, diary_id, actor=current_user.user) + + +@router.post("/{diary_id}/archive", response_model=DiarySummary) +def archive_diary_endpoint( + diary_id: str, + archived: bool = Query(default=True), + current_user: CurrentUser = Depends(require_permission("menu.manage")), + db: Session = Depends(get_db), +) -> DiarySummary: + return archive_diary(db, diary_id, archived, actor=current_user.user) diff --git a/api/app/api/v1/mermaids.py b/api/app/api/v1/mermaids.py new file mode 100644 index 0000000..29b0e54 --- /dev/null +++ b/api/app/api/v1/mermaids.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session + +from ...core.database import get_db +from ...core.dependencies import CurrentUser, require_any_permission +from ...schemas.mermaid import ( + MermaidChatStreamRequest, + MermaidDiagramCreateRequest, + MermaidDiagramDataPatchRequest, + MermaidDiagramPageResponse, + MermaidDiagramQueryRequest, + MermaidDiagramSummary, + MermaidDiagramUpdateRequest, + MermaidGroupListResponse, +) +from ...services.mermaid_service import ( + create_mermaid_diagram, + delete_mermaid_diagram, + get_mermaid_diagram_summary, + list_mermaid_groups, + search_mermaid_diagrams, + stream_chat_mermaid_code, + stream_generate_mermaid_code, + update_mermaid_diagram, + update_mermaid_diagram_data, +) + +router = APIRouter(prefix="/mermaids/diagrams", tags=["mermaids"]) + + +@router.post("/search", response_model=MermaidDiagramPageResponse) +def search_mermaid_endpoint( + payload: MermaidDiagramQueryRequest, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MermaidDiagramPageResponse: + return search_mermaid_diagrams(db, payload, actor=current_user.user) + + +@router.get("/groups", response_model=MermaidGroupListResponse) +def list_mermaid_group_endpoint( + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MermaidGroupListResponse: + return list_mermaid_groups(db, actor=current_user.user) + + +@router.get("/get/{diagram_id}", response_model=MermaidDiagramSummary) +def get_mermaid_endpoint( + diagram_id: str, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MermaidDiagramSummary: + item = get_mermaid_diagram_summary(db, diagram_id, actor=current_user.user) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mermaid diagram not found") + return item + + +@router.post("/create", response_model=MermaidDiagramSummary) +def create_mermaid_endpoint( + payload: MermaidDiagramCreateRequest, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MermaidDiagramSummary: + return create_mermaid_diagram(db, payload, actor=current_user.user) + + +@router.put("/update", response_model=MermaidDiagramSummary) +def update_mermaid_endpoint( + payload: MermaidDiagramUpdateRequest, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MermaidDiagramSummary: + return update_mermaid_diagram(db, payload, actor=current_user.user) + + +@router.delete("/delete/{diagram_id}") +def delete_mermaid_endpoint( + diagram_id: str, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> dict[str, bool]: + return delete_mermaid_diagram(db, diagram_id, actor=current_user.user) + + +@router.patch("/{diagram_id}/data", response_model=MermaidDiagramSummary) +def update_mermaid_data_endpoint( + diagram_id: str, + payload: MermaidDiagramDataPatchRequest, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MermaidDiagramSummary: + return update_mermaid_diagram_data(db, diagram_id, payload, actor=current_user.user) + + +@router.get("/generate/stream") +async def generate_mermaid_stream_endpoint( + advice: str = Query(min_length=1), + diagram_data: str | None = Query(default=None, alias="diagramData"), + model_name: str | None = Query(default=None, alias="modelName"), + _: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> StreamingResponse: + async def event_gen() -> AsyncGenerator[str, None]: + async for chunk in stream_generate_mermaid_code( + db, + advice=advice, + diagram_data=diagram_data, + model_name=model_name, + ): + yield f"data: {chunk}\n\n" + + return StreamingResponse( + event_gen(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) + + +@router.post("/chat/stream") +async def chat_mermaid_stream_endpoint( + payload: MermaidChatStreamRequest, + _: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> StreamingResponse: + async def event_gen() -> AsyncGenerator[str, None]: + async for chunk in stream_chat_mermaid_code(db, payload): + yield f"data: {chunk}\n\n" + + return StreamingResponse( + event_gen(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) diff --git a/api/app/api/v1/mind_map.py b/api/app/api/v1/mind_map.py new file mode 100644 index 0000000..b6fd78c --- /dev/null +++ b/api/app/api/v1/mind_map.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session + +from ...core.database import get_db +from ...core.dependencies import CurrentUser, require_any_permission, require_permission +from ...schemas.mind_map import ( + MindMapBasicInfoUpdateRequest, + MindMapCreateRequest, + MindMapDataUpdateRequest, + MindMapPageResponse, + MindMapQueryRequest, + MindMapSummary, +) +from ...services.mind_map_service import ( + create_mind_map, + delete_mind_map, + get_mind_map_by_id, + search_mind_maps, + serialize_mind_map, + stream_generate_mind_map, + update_mind_map_basic_info, + update_mind_map_data, +) + +router = APIRouter(prefix="/mindmap", tags=["mindmap"]) + + +@router.post("/search", response_model=MindMapPageResponse) +def search_mind_map_endpoint( + payload: MindMapQueryRequest, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MindMapPageResponse: + return search_mind_maps(db, payload, actor=current_user.user) + + +@router.get("/get/{mind_map_id}", response_model=MindMapSummary) +def get_mind_map_endpoint( + mind_map_id: str, + current_user: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> MindMapSummary: + item = get_mind_map_by_id(db, mind_map_id, actor=current_user.user) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mind map not found") + return serialize_mind_map(item) + + +@router.post("/create", response_model=MindMapSummary) +def create_mind_map_endpoint( + payload: MindMapCreateRequest, + current_user: CurrentUser = Depends(require_permission("question_bank.manage")), + db: Session = Depends(get_db), +) -> MindMapSummary: + return create_mind_map(db, payload, actor=current_user.user) + + +@router.put("/update-basic-info", response_model=MindMapSummary) +def update_mind_map_basic_info_endpoint( + payload: MindMapBasicInfoUpdateRequest, + current_user: CurrentUser = Depends(require_permission("question_bank.manage")), + db: Session = Depends(get_db), +) -> MindMapSummary: + return update_mind_map_basic_info(db, payload, actor=current_user.user) + + +@router.put("/update-data", response_model=MindMapSummary) +def update_mind_map_data_endpoint( + payload: MindMapDataUpdateRequest, + current_user: CurrentUser = Depends(require_permission("question_bank.manage")), + db: Session = Depends(get_db), +) -> MindMapSummary: + return update_mind_map_data(db, payload, actor=current_user.user) + + +@router.delete("/delete/{mind_map_id}") +def delete_mind_map_endpoint( + mind_map_id: str, + current_user: CurrentUser = Depends(require_permission("question_bank.manage")), + db: Session = Depends(get_db), +) -> dict[str, bool]: + return delete_mind_map(db, mind_map_id, actor=current_user.user) + + +@router.get("/generate/stream") +async def generate_mind_map_stream_endpoint( + descr: str = Query(min_length=1), + _: CurrentUser = Depends(require_any_permission("question_bank.read", "question_bank.manage")), + db: Session = Depends(get_db), +) -> StreamingResponse: + async def event_gen() -> AsyncGenerator[str, None]: + async for chunk in stream_generate_mind_map(db, descr=descr): + yield f"data: {chunk}\n\n" + + return StreamingResponse( + event_gen(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) diff --git a/api/app/api/v1/todos.py b/api/app/api/v1/todos.py index bc47952..9b5503e 100644 --- a/api/app/api/v1/todos.py +++ b/api/app/api/v1/todos.py @@ -6,14 +6,17 @@ from ...core.dependencies import CurrentUser, require_any_permission, require_pe from ...schemas.todo import ( TodoCreateRequest, TodoListResponse, + TodoMindMapInitResponse, TodoSummary, TodoTransitionRequest, TodoUpdateRequest, ) from ...services.todo_service import ( + complete_todo, create_todo, delete_todo, get_todo_by_id, + init_todo_mindmap, list_todos, serialize_todo, transition_todo, @@ -25,19 +28,23 @@ router = APIRouter(prefix="/todos", tags=["todos"]) @router.get("", response_model=TodoListResponse) def get_todo_list( + title: str | None = Query(default=None), keyword: str | None = Query(default=None), status_filter: str | None = Query(default=None, alias="status"), priority: str | None = Query(default=None), - assignee_user_id: str | None = Query(default=None), - _: CurrentUser = Depends(require_permission("todo.read")), + page_num: int = Query(default=0, ge=0), + page_size: int = Query(default=20, ge=1, le=200), + current_user: CurrentUser = Depends(require_permission("todo.read")), db: Session = Depends(get_db), ) -> TodoListResponse: return list_todos( db, - keyword=keyword, + title=title or keyword, status_filter=status_filter, priority=priority, - assignee_user_id=assignee_user_id, + page_num=page_num, + page_size=page_size, + actor=current_user.user, ) @@ -53,10 +60,10 @@ def create_todo_endpoint( @router.get("/{todo_id}", response_model=TodoSummary) def get_todo_detail( todo_id: str, - _: CurrentUser = Depends(require_permission("todo.read")), + current_user: CurrentUser = Depends(require_permission("todo.read")), db: Session = Depends(get_db), ) -> TodoSummary: - todo = get_todo_by_id(db, todo_id) + todo = get_todo_by_id(db, todo_id, actor=current_user.user) if not todo: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") return serialize_todo(todo) @@ -82,6 +89,24 @@ def transition_todo_endpoint( return transition_todo(db, todo_id, payload, actor=current_user.user) +@router.post("/{todo_id}/complete", response_model=TodoSummary) +def complete_todo_endpoint( + todo_id: str, + current_user: CurrentUser = Depends(require_any_permission("todo.process", "todo.manage")), + db: Session = Depends(get_db), +) -> TodoSummary: + return complete_todo(db, todo_id, actor=current_user.user) + + +@router.post("/{todo_id}/init-mindmap", response_model=TodoMindMapInitResponse) +def init_todo_mindmap_endpoint( + todo_id: str, + current_user: CurrentUser = Depends(require_permission("todo.read")), + db: Session = Depends(get_db), +) -> TodoMindMapInitResponse: + return init_todo_mindmap(db, todo_id, actor=current_user.user) + + @router.delete("/{todo_id}") def delete_todo_endpoint( todo_id: str, diff --git a/api/app/core/config.py b/api/app/core/config.py index bfbdcce..220015b 100644 --- a/api/app/core/config.py +++ b/api/app/core/config.py @@ -2,6 +2,7 @@ from functools import lru_cache import json import re from typing import Literal +from urllib.parse import quote_plus from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -15,7 +16,13 @@ class Settings(BaseSettings): api_cors_origins: str = "http://localhost:3000,http://127.0.0.1:3000" api_cors_origin_regex: str | None = None - database_url: str = "sqlite:///./fquiz.db" + database_url: str | None = None + db_host: str = "223.109.142.84" + db_port: int = 5432 + db_name: str = "postgres" + db_schema: str = "public" + db_username: str = "postgres" + db_password: str = "1qazZAQ!" file_vfs_root: str = "./data/vfs" jwt_secret_key: str = "change-this-in-production" @@ -48,6 +55,7 @@ class Settings(BaseSettings): "refresh_token_expire_days", "llm_request_timeout_seconds", "chat_context_message_limit", + "db_port", ) @classmethod def validate_positive_numbers(cls, value: int) -> int: @@ -126,6 +134,25 @@ class Settings(BaseSettings): mapping[provider_key] = secret return mapping + @property + def resolved_database_url(self) -> str: + explicit_database_url = (self.database_url or "").strip() + if explicit_database_url: + return explicit_database_url + + username = quote_plus(self.db_username.strip()) + password = quote_plus(self.db_password.strip()) + host = self.db_host.strip() + database = self.db_name.strip() + return f"postgresql+psycopg://{username}:{password}@{host}:{self.db_port}/{database}" + + @property + def resolved_db_schema(self) -> str | None: + schema = self.db_schema.strip() + if not schema: + return None + return schema + @lru_cache def get_settings() -> Settings: diff --git a/api/app/core/database.py b/api/app/core/database.py index e9f91c4..c3c8d2f 100644 --- a/api/app/core/database.py +++ b/api/app/core/database.py @@ -1,4 +1,6 @@ from collections.abc import Generator +import logging +from typing import Any from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker @@ -6,13 +8,20 @@ from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker from .config import get_settings settings = get_settings() +logger = logging.getLogger(__name__) -connect_args: dict[str, bool] = {} -if settings.database_url.startswith("sqlite"): +database_url = settings.resolved_database_url + +connect_args: dict[str, Any] = {} +if database_url.startswith("sqlite"): connect_args["check_same_thread"] = False +elif database_url.startswith("postgresql"): + schema = settings.resolved_db_schema + if schema: + connect_args["options"] = f"-csearch_path={schema}" engine = create_engine( - settings.database_url, + database_url, pool_pre_ping=True, connect_args=connect_args, ) @@ -42,12 +51,17 @@ def init_db() -> None: from ..models import ( audit_log, auth_session, + calendar_event, chat, + diary, file_storage, hot_search, life_countdown, menu, + mermaid_diagram, + mind_map, model_registry, + object_group, question_bank, rbac, requirement, @@ -61,4 +75,20 @@ def init_db() -> None: Base.metadata.create_all(bind=engine) with SessionLocal() as db: - seed_defaults(db) + local_hosts = {"db", "localhost", "127.0.0.1", "::1"} + database_url = (settings.database_url or "").strip().lower() + database_url_targets_local = any( + token in database_url for token in ("@db:", "@localhost:", "@127.0.0.1:", "@[::1]:") + ) + should_seed_defaults = ( + settings.db_host.strip().lower() in local_hosts + or database_url_targets_local + ) + + if should_seed_defaults: + seed_defaults(db) + else: + logger.info( + "Skip seed defaults for non-local database target: host=%s", + settings.db_host, + ) diff --git a/api/app/models/__init__.py b/api/app/models/__init__.py index 36e4c02..7b15e7d 100644 --- a/api/app/models/__init__.py +++ b/api/app/models/__init__.py @@ -4,17 +4,22 @@ Import all model modules during package initialization so SQLAlchemy can resolve string-based relationships regardless of route/service import order. """ -from . import audit_log, auth_session, chat, file_storage, hot_search, life_countdown, menu, model_registry, question_bank, rbac, requirement, system_message, system_param, todo, user, vocabulary_word +from . import audit_log, auth_session, calendar_event, chat, diary, file_storage, hot_search, life_countdown, menu, mermaid_diagram, mind_map, model_registry, object_group, question_bank, rbac, requirement, system_message, system_param, todo, user, vocabulary_word __all__ = [ "audit_log", "auth_session", + "calendar_event", "chat", + "diary", "file_storage", "hot_search", "life_countdown", "menu", + "mermaid_diagram", + "mind_map", "model_registry", + "object_group", "question_bank", "rbac", "requirement", diff --git a/api/app/models/audit_log.py b/api/app/models/audit_log.py index e9b80c4..ef2bd9d 100644 --- a/api/app/models/audit_log.py +++ b/api/app/models/audit_log.py @@ -19,7 +19,7 @@ class AuditLog(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) action: Mapped[str] = mapped_column(String(128), index=True) diff --git a/api/app/models/auth_session.py b/api/app/models/auth_session.py index 10a5d8d..4225639 100644 --- a/api/app/models/auth_session.py +++ b/api/app/models/auth_session.py @@ -24,7 +24,7 @@ class AuthSession(Base): ) user_id: Mapped[str] = mapped_column( String(36), - ForeignKey("users.id", ondelete="CASCADE"), + ForeignKey("users.user_id", ondelete="CASCADE"), index=True, ) refresh_token_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True) diff --git a/api/app/models/calendar_event.py b/api/app/models/calendar_event.py new file mode 100644 index 0000000..634235a --- /dev/null +++ b/api/app/models/calendar_event.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import uuid4 + +from sqlalchemy import Boolean, DateTime, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from ..core.database import Base +from .base import utcnow + + +class CalendarEvent(Base): + __tablename__ = "calendar_event" + + id: Mapped[str] = mapped_column( + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) + title: Mapped[str] = mapped_column(String(256), index=True) + descr: Mapped[str] = mapped_column(Text(), default="") + status: Mapped[str] = mapped_column(String(20), default="SCHEDULED", index=True) + priority: Mapped[str] = mapped_column(String(20), default="MEDIUM", index=True) + + start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + end_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) + expire_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) + + all_day: Mapped[bool] = mapped_column(Boolean, default=False) + completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + + todo_id: Mapped[str | None] = mapped_column(String(32), index=True) + + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) + create_user: Mapped[str] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, + ) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) diff --git a/api/app/models/chat.py b/api/app/models/chat.py index 853125a..84719cf 100644 --- a/api/app/models/chat.py +++ b/api/app/models/chat.py @@ -24,7 +24,7 @@ class ChatSession(Base): ) owner_user_id: Mapped[str] = mapped_column( String(36), - ForeignKey("users.id", ondelete="CASCADE"), + ForeignKey("users.user_id", ondelete="CASCADE"), index=True, ) title: Mapped[str] = mapped_column(String(200), default="新会话") @@ -59,7 +59,7 @@ class ChatMessage(Base): ) author_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) role: Mapped[str] = mapped_column(String(16), index=True) diff --git a/api/app/models/diary.py b/api/app/models/diary.py new file mode 100644 index 0000000..92e8c06 --- /dev/null +++ b/api/app/models/diary.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from datetime import date, datetime +from uuid import uuid4 + +from sqlalchemy import Boolean, Date, DateTime, Index, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from ..core.database import Base +from .base import utcnow + + +class Diary(Base): + __tablename__ = "diary" + __table_args__ = ( + Index("idx_diary_create_user", "create_user"), + Index("idx_diary_diary_date", "diary_date"), + Index("idx_diary_mood", "mood"), + Index("idx_diary_archived", "archived"), + ) + + id: Mapped[str] = mapped_column( + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) + title: Mapped[str] = mapped_column(String(256), nullable=False) + content: Mapped[str] = mapped_column(Text(), nullable=False) + diary_date: Mapped[date] = mapped_column(Date(), nullable=False, index=True) + mood: Mapped[str] = mapped_column(String(20), nullable=False, default="CALM", index=True) + weather: Mapped[str | None] = mapped_column(String(64)) + archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True) + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, + index=True, + ) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) diff --git a/api/app/models/file_storage.py b/api/app/models/file_storage.py index 3c41b8f..7b05afb 100644 --- a/api/app/models/file_storage.py +++ b/api/app/models/file_storage.py @@ -107,7 +107,7 @@ class FileIndexEntry(Base): ) last_synced_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) last_synced_by_user: Mapped[User | None] = relationship( diff --git a/api/app/models/hot_search.py b/api/app/models/hot_search.py index cd0be1d..9a06137 100644 --- a/api/app/models/hot_search.py +++ b/api/app/models/hot_search.py @@ -30,12 +30,12 @@ class HotSearchRecord(Base): matched_topics_json: Mapped[list[str] | None] = mapped_column(JSON, default=None) creator_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) updater_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) @@ -67,12 +67,12 @@ class HotSearchFollowTopic(Base): seq: Mapped[int] = mapped_column(Integer, default=0, index=True) creator_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) updater_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/api/app/models/life_countdown.py b/api/app/models/life_countdown.py index 8dc0c9a..595f49b 100644 --- a/api/app/models/life_countdown.py +++ b/api/app/models/life_countdown.py @@ -24,7 +24,7 @@ class LifeCountdownProfile(Base): ) user_id: Mapped[str] = mapped_column( String(36), - ForeignKey("users.id", ondelete="CASCADE"), + ForeignKey("users.user_id", ondelete="CASCADE"), unique=True, index=True, ) diff --git a/api/app/models/mermaid_diagram.py b/api/app/models/mermaid_diagram.py new file mode 100644 index 0000000..a3e272b --- /dev/null +++ b/api/app/models/mermaid_diagram.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import uuid4 + +from sqlalchemy import DateTime, Index, Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from ..core.database import Base +from .base import utcnow + + +class MermaidDiagram(Base): + __tablename__ = "mermaid_diagram" + __table_args__ = ( + Index("idx_mermaid_diagram_name", "diagram_name"), + Index("idx_mermaid_diagram_create_date", "create_date"), + ) + + id: Mapped[str] = mapped_column( + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) + diagram_name: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str | None] = mapped_column(Text(), default="") + diagram_data: Mapped[str | None] = mapped_column(Text(), default="") + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, + ) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) + + +class MermaidDiagramHistory(Base): + __tablename__ = "mermaid_diagram_history" + __table_args__ = ( + Index("idx_mermaid_history_diagram_id", "diagram_id"), + Index("idx_mermaid_history_create_date", "create_date"), + ) + + id: Mapped[str] = mapped_column( + "history_id", + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) + diagram_id: Mapped[str] = mapped_column(String(32), nullable=False, index=True) + version_num: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + diagram_data: Mapped[str | None] = mapped_column(Text(), default="") + description: Mapped[str | None] = mapped_column(String(255)) + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) diff --git a/api/app/models/mind_map.py b/api/app/models/mind_map.py new file mode 100644 index 0000000..c77ad02 --- /dev/null +++ b/api/app/models/mind_map.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import uuid4 + +from sqlalchemy import DateTime, Index, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from ..core.database import Base +from .base import utcnow + + +class MindMap(Base): + __tablename__ = "mind_map" + __table_args__ = ( + Index("idx_mind_map_name", "map_name"), + Index("idx_mind_map_create_date", "create_date"), + ) + + id: Mapped[str] = mapped_column( + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) + map_name: Mapped[str] = mapped_column(String(255), nullable=False) + descr: Mapped[str | None] = mapped_column(Text(), default="") + map_data: Mapped[str | None] = mapped_column(Text(), default="") + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, + ) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) diff --git a/api/app/models/model_registry.py b/api/app/models/model_registry.py index 211609f..e1cac0d 100644 --- a/api/app/models/model_registry.py +++ b/api/app/models/model_registry.py @@ -123,7 +123,7 @@ class ModelApiKey(Base): rotation_note: Mapped[str | None] = mapped_column(String(255)) created_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) @@ -165,7 +165,7 @@ class ModelTestRun(Base): error_message: Mapped[str | None] = mapped_column(Text()) created_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/api/app/models/object_group.py b/api/app/models/object_group.py new file mode 100644 index 0000000..a003f07 --- /dev/null +++ b/api/app/models/object_group.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import uuid4 + +from sqlalchemy import DateTime, ForeignKey, Index, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column + +from ..core.database import Base +from .base import utcnow + + +class ObjectGroup(Base): + __tablename__ = "obj_group" + __table_args__ = ( + UniqueConstraint("name", "type", "create_user", name="uq_obj_group_name_type_create_user"), + Index("idx_obj_group_type", "type"), + ) + + id: Mapped[str] = mapped_column( + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) + name: Mapped[str] = mapped_column(String(128), nullable=False, index=True) + label: Mapped[str] = mapped_column(String(256), nullable=False) + type: Mapped[str | None] = mapped_column(String(64), index=True) + descr: Mapped[str | None] = mapped_column(String(512)) + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, + ) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) + + +class ObjectGroupRelation(Base): + __tablename__ = "obj_group_obj_rela" + __table_args__ = ( + UniqueConstraint("group_id", "obj_id", name="uq_obj_group_obj_rela_group_obj"), + Index("idx_obj_group_obj_rela_group", "group_id"), + Index("idx_obj_group_obj_rela_obj", "obj_id"), + ) + + rela_id: Mapped[str] = mapped_column( + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) + group_id: Mapped[str] = mapped_column( + String(32), + ForeignKey("obj_group.id", ondelete="CASCADE"), + nullable=False, + ) + obj_id: Mapped[str] = mapped_column(String(32), nullable=False) diff --git a/api/app/models/question_bank.py b/api/app/models/question_bank.py index 87ae2e2..ebb79a2 100644 --- a/api/app/models/question_bank.py +++ b/api/app/models/question_bank.py @@ -27,12 +27,12 @@ class QuestionBank(Base): tags_json: Mapped[list[str] | None] = mapped_column(JSON) creator_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) updater_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/api/app/models/rbac.py b/api/app/models/rbac.py index 9129827..f28e8b1 100644 --- a/api/app/models/rbac.py +++ b/api/app/models/rbac.py @@ -59,7 +59,7 @@ class UserRole(Base): user_id: Mapped[str] = mapped_column( String(36), - ForeignKey("users.id", ondelete="CASCADE"), + ForeignKey("users.user_id", ondelete="CASCADE"), primary_key=True, ) role_id: Mapped[int] = mapped_column( diff --git a/api/app/models/requirement.py b/api/app/models/requirement.py index 873d34b..c50bf95 100644 --- a/api/app/models/requirement.py +++ b/api/app/models/requirement.py @@ -1,10 +1,10 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from uuid import uuid4 -from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy import DateTime, ForeignKey, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from ..core.database import Base @@ -15,60 +15,31 @@ if TYPE_CHECKING: class Requirement(Base): - __tablename__ = "requirements" + __tablename__ = "project_requirement" id: Mapped[str] = mapped_column( - String(36), + String(32), primary_key=True, - default=lambda: str(uuid4()), + default=lambda: uuid4().hex, ) - code: Mapped[str] = mapped_column(String(64), unique=True, index=True) - title: Mapped[str] = mapped_column(String(200), index=True) - description: Mapped[str] = mapped_column(Text(), default="") - status: Mapped[str] = mapped_column(String(32), default="PENDING_ANALYSIS", index=True) - priority: Mapped[str] = mapped_column(String(16), default="medium", index=True) + title: Mapped[str] = mapped_column(String(256), index=True) project_name: Mapped[str | None] = mapped_column(String(128), index=True) - module_name: Mapped[str | None] = mapped_column(String(128), index=True) - source: Mapped[str | None] = mapped_column(String(128), index=True) - creator_user_id: Mapped[str | None] = mapped_column( - String(36), - ForeignKey("users.id", ondelete="SET NULL"), - index=True, - ) - assignee_user_id: Mapped[str | None] = mapped_column( - String(36), - ForeignKey("users.id", ondelete="SET NULL"), - index=True, - ) - reviewer_user_id: Mapped[str | None] = mapped_column( - String(36), - ForeignKey("users.id", ondelete="SET NULL"), - index=True, - ) - due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) - closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) - updated_at: Mapped[datetime] = mapped_column( + git_url: Mapped[str | None] = mapped_column(String(512)) + branch: Mapped[str | None] = mapped_column(String(128), default="main") + descr: Mapped[str] = mapped_column(Text(), default="") + result_msg: Mapped[str | None] = mapped_column(Text()) + progress_percent: Mapped[int] = mapped_column(Integer, default=0) + status: Mapped[str] = mapped_column(String(30), default="PENDING_ANALYSIS", index=True) + priority: Mapped[str] = mapped_column(String(20), default="MEDIUM", index=True) + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, ) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) - creator: Mapped[User | None] = relationship( - "User", - foreign_keys=[creator_user_id], - lazy="selectin", - ) - assignee: Mapped[User | None] = relationship( - "User", - foreign_keys=[assignee_user_id], - lazy="selectin", - ) - reviewer: Mapped[User | None] = relationship( - "User", - foreign_keys=[reviewer_user_id], - lazy="selectin", - ) comments: Mapped[list[RequirementComment]] = relationship( "RequirementComment", back_populates="requirement", @@ -81,7 +52,7 @@ class Requirement(Base): back_populates="requirement", lazy="selectin", cascade="all, delete-orphan", - order_by="RequirementEvent.created_at.desc()", + order_by="RequirementEvent.create_date.desc()", ) @@ -90,13 +61,13 @@ class RequirementComment(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) requirement_id: Mapped[str] = mapped_column( - String(36), - ForeignKey("requirements.id", ondelete="CASCADE"), + String(32), + ForeignKey("project_requirement.id", ondelete="CASCADE"), index=True, ) author_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) content: Mapped[str] = mapped_column(Text()) @@ -112,28 +83,31 @@ class RequirementComment(Base): class RequirementEvent(Base): - __tablename__ = "requirement_events" + __tablename__ = "project_requirement_log" - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + id: Mapped[str] = mapped_column( + String(32), + primary_key=True, + default=lambda: uuid4().hex, + ) requirement_id: Mapped[str] = mapped_column( - String(36), - ForeignKey("requirements.id", ondelete="CASCADE"), + String(32), + ForeignKey("project_requirement.id", ondelete="CASCADE"), index=True, ) - actor_user_id: Mapped[str | None] = mapped_column( - String(36), - ForeignKey("users.id", ondelete="SET NULL"), - index=True, + event_type: Mapped[str] = mapped_column(String(30), index=True) + from_status: Mapped[str | None] = mapped_column(String(30), index=True) + to_status: Mapped[str | None] = mapped_column(String(30), index=True) + before_descr: Mapped[str | None] = mapped_column(Text()) + after_descr: Mapped[str | None] = mapped_column(Text()) + remark: Mapped[str | None] = mapped_column(Text()) + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, ) - event_type: Mapped[str] = mapped_column(String(64), index=True) - from_status: Mapped[str | None] = mapped_column(String(32), index=True) - to_status: Mapped[str | None] = mapped_column(String(32), index=True) - payload_json: Mapped[dict[str, Any] | None] = mapped_column(JSON) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) requirement: Mapped[Requirement] = relationship("Requirement", back_populates="events") - actor: Mapped[User | None] = relationship( - "User", - foreign_keys=[actor_user_id], - lazy="selectin", - ) diff --git a/api/app/models/system_message.py b/api/app/models/system_message.py index 4b267d8..7829d5c 100644 --- a/api/app/models/system_message.py +++ b/api/app/models/system_message.py @@ -25,12 +25,12 @@ class SystemMessage(Base): end_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) updated_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/api/app/models/system_param.py b/api/app/models/system_param.py index 051aefb..4911b52 100644 --- a/api/app/models/system_param.py +++ b/api/app/models/system_param.py @@ -24,12 +24,12 @@ class SystemParam(Base): status: Mapped[str] = mapped_column(String(16), default="enabled", index=True) created_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) updated_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/api/app/models/todo.py b/api/app/models/todo.py index 9b3b0cc..3028ad5 100644 --- a/api/app/models/todo.py +++ b/api/app/models/todo.py @@ -1,57 +1,42 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING from uuid import uuid4 -from sqlalchemy import DateTime, ForeignKey, String, Text -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import DateTime, Index, String, Text +from sqlalchemy.orm import Mapped, mapped_column from ..core.database import Base from .base import utcnow -if TYPE_CHECKING: - from .user import User - class Todo(Base): - __tablename__ = "todos" + __tablename__ = "todo" + __table_args__ = ( + Index("idx_todo_status", "status"), + Index("idx_todo_priority", "priority"), + Index("idx_todo_due_date", "due_date"), + Index("idx_todo_expire_time", "expire_time"), + ) id: Mapped[str] = mapped_column( - String(36), + String(32), primary_key=True, - default=lambda: str(uuid4()), + default=lambda: uuid4().hex, ) - title: Mapped[str] = mapped_column(String(200), index=True) - description: Mapped[str] = mapped_column(Text(), default="") - status: Mapped[str] = mapped_column(String(32), default="TODO", index=True) - priority: Mapped[str] = mapped_column(String(16), default="medium", index=True) - assignee_user_id: Mapped[str | None] = mapped_column( - String(36), - ForeignKey("users.id", ondelete="SET NULL"), - index=True, - ) - creator_user_id: Mapped[str | None] = mapped_column( - String(36), - ForeignKey("users.id", ondelete="SET NULL"), - index=True, - ) - due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) - completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) - updated_at: Mapped[datetime] = mapped_column( + title: Mapped[str] = mapped_column(String(256), nullable=False) + descr: Mapped[str | None] = mapped_column(Text(), default="") + status: Mapped[str] = mapped_column(String(20), default="SCHEDULED", nullable=False) + priority: Mapped[str] = mapped_column(String(20), default="MEDIUM", nullable=False) + start_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + due_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + expire_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + calendar_event_id: Mapped[str | None] = mapped_column(String(32)) + create_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) + create_user: Mapped[str | None] = mapped_column(String(64), index=True) + update_date: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, ) - - creator: Mapped[User | None] = relationship( - "User", - foreign_keys=[creator_user_id], - lazy="selectin", - ) - assignee: Mapped[User | None] = relationship( - "User", - foreign_keys=[assignee_user_id], - lazy="selectin", - ) + update_user: Mapped[str | None] = mapped_column(String(64), index=True) diff --git a/api/app/models/user.py b/api/app/models/user.py index 5e1f36d..45d38fe 100644 --- a/api/app/models/user.py +++ b/api/app/models/user.py @@ -20,24 +20,37 @@ class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column( + "user_id", String(36), primary_key=True, default=lambda: str(uuid4()), ) email: Mapped[str] = mapped_column(String(255), unique=True, index=True) - username: Mapped[str] = mapped_column(String(64), unique=True, index=True) - password_hash: Mapped[str] = mapped_column(String(255)) - status: Mapped[str] = mapped_column(String(32), default="active", index=True) - last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + username: Mapped[str] = mapped_column("user_name", String(64), unique=True, index=True) + password_hash: Mapped[str] = mapped_column("password", String(255)) + status: Mapped[str] = mapped_column("state", String(32), default="active", index=True) created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), + "create_date", + DateTime(timezone=False), default=utcnow, ) updated_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), + "update_date", + DateTime(timezone=False), default=utcnow, onupdate=utcnow, ) + create_user: Mapped[str | None] = mapped_column(String(64), nullable=True) + update_user: Mapped[str | None] = mapped_column(String(64), nullable=True) + + @property + def last_login_at(self) -> datetime | None: + return self.updated_at + + @last_login_at.setter + def last_login_at(self, value: datetime | None) -> None: + if value is not None: + self.updated_at = value roles: Mapped[list[Role]] = relationship( "Role", diff --git a/api/app/models/vocabulary_word.py b/api/app/models/vocabulary_word.py index 736b4f0..ce2339a 100644 --- a/api/app/models/vocabulary_word.py +++ b/api/app/models/vocabulary_word.py @@ -24,12 +24,12 @@ class VocabularyWord(Base): status: Mapped[str] = mapped_column(String(16), default="enabled", index=True) created_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) updated_by_user_id: Mapped[str | None] = mapped_column( String(36), - ForeignKey("users.id", ondelete="SET NULL"), + ForeignKey("users.user_id", ondelete="SET NULL"), index=True, ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) diff --git a/api/app/schemas/calendar_event.py b/api/app/schemas/calendar_event.py new file mode 100644 index 0000000..84de635 --- /dev/null +++ b/api/app/schemas/calendar_event.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, Field + +ScheduleStatus = Literal["SCHEDULED", "IN_PROGRESS", "COMPLETED", "CANCELLED", "EXPIRED"] +SchedulePriority = Literal["LOW", "MEDIUM", "HIGH"] + + +class CalendarEventSummary(BaseModel): + id: str + title: str + descr: str + status: ScheduleStatus + priority: SchedulePriority + start_time: datetime + end_time: datetime + expire_time: datetime | None = None + all_day: bool = False + completed_at: datetime | None = None + todo_id: str | None = None + create_date: datetime + create_user: str + update_date: datetime + update_user: str | None = None + + +class CalendarEventPageResponse(BaseModel): + items: list[CalendarEventSummary] + total: int + page_num: int + page_size: int + + +class CalendarEventCreateRequest(BaseModel): + title: str = Field(min_length=1, max_length=256) + descr: str = Field(default="", max_length=100000) + status: ScheduleStatus = "SCHEDULED" + priority: SchedulePriority = "MEDIUM" + start_time: datetime + end_time: datetime + expire_time: datetime | None = None + all_day: bool = False + + # Internal sync flags (used by services, not by UI directly). + is_sync: bool = False + todo_id: str | None = Field(default=None, max_length=32) + + +class CalendarEventUpdateRequest(BaseModel): + id: str = Field(min_length=1, max_length=32) + title: str | None = Field(default=None, min_length=1, max_length=256) + descr: str | None = Field(default=None, max_length=100000) + status: ScheduleStatus | None = None + priority: SchedulePriority | None = None + start_time: datetime | None = None + end_time: datetime | None = None + expire_time: datetime | None = None + all_day: bool | None = None + completed_at: datetime | None = None + + # Internal sync flag. + is_sync: bool = False + + +class CalendarEventQueryRequest(BaseModel): + title: str | None = Field(default=None, max_length=256) + status: ScheduleStatus | None = None + priority: SchedulePriority | None = None + start_time_from: datetime | None = None + start_time_to: datetime | None = None + page_num: int = Field(default=0, ge=0) + page_size: int = Field(default=50, ge=1, le=500) diff --git a/api/app/schemas/diary.py b/api/app/schemas/diary.py new file mode 100644 index 0000000..c36672d --- /dev/null +++ b/api/app/schemas/diary.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from datetime import date, datetime +from typing import Literal + +from pydantic import BaseModel, Field + +DiaryMood = Literal["HAPPY", "CALM", "SAD", "ANGRY", "TIRED", "EXCITED"] + + +class DiarySummary(BaseModel): + id: str + title: str + content: str + diary_date: date + mood: DiaryMood + weather: str | None = None + archived: bool = False + create_date: datetime + create_user: str | None = None + update_date: datetime + update_user: str | None = None + + +class DiaryPageResponse(BaseModel): + items: list[DiarySummary] + total: int + page_num: int + page_size: int + + +class DiaryQueryRequest(BaseModel): + title: str | None = Field(default=None, max_length=256) + mood: DiaryMood | None = None + diary_date_start: date | None = None + diary_date_end: date | None = None + archived: bool | None = None + page_num: int = Field(default=0, ge=0) + page_size: int = Field(default=20, ge=1, le=200) + + +class DiaryCreateRequest(BaseModel): + title: str = Field(min_length=1, max_length=256) + content: str = Field(min_length=1, max_length=200000) + diary_date: date = Field(default_factory=date.today) + mood: DiaryMood = "CALM" + weather: str | None = Field(default=None, max_length=64) + archived: bool = False + + +class DiaryUpdateRequest(BaseModel): + id: str = Field(min_length=1, max_length=32) + title: str = Field(min_length=1, max_length=256) + content: str = Field(min_length=1, max_length=200000) + diary_date: date + mood: DiaryMood + weather: str | None = Field(default=None, max_length=64) + archived: bool = False diff --git a/api/app/schemas/mermaid.py b/api/app/schemas/mermaid.py new file mode 100644 index 0000000..de44dfb --- /dev/null +++ b/api/app/schemas/mermaid.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Literal + +from pydantic import AliasChoices, BaseModel, ConfigDict, Field + + +class MermaidRequestModel(BaseModel): + model_config = ConfigDict(populate_by_name=True, extra="ignore") + + +class MermaidGroupSummary(BaseModel): + id: str + name: str + label: str + type: str | None = None + descr: str | None = None + + +class MermaidGroupListResponse(BaseModel): + items: list[MermaidGroupSummary] + total: int + + +class MermaidDiagramSummary(BaseModel): + id: str + diagram_name: str + description: str | None = None + diagram_data: str | None = None + group_name: str | None = None + group_label: str | None = None + tag_names: list[str] = Field(default_factory=list) + tag_labels: list[str] = Field(default_factory=list) + create_date: datetime + create_user: str | None = None + update_date: datetime + update_user: str | None = None + + +class MermaidDiagramPageResponse(BaseModel): + items: list[MermaidDiagramSummary] + total: int + page_num: int + page_size: int + + +class MermaidDiagramQueryRequest(MermaidRequestModel): + key_word: str | None = Field(default=None, max_length=255, validation_alias=AliasChoices("key_word", "keyWord")) + group: str | None = Field(default=None, max_length=128) + tags: list[str] | None = None + page_num: int = Field(default=0, ge=0, validation_alias=AliasChoices("page_num", "pageNum")) + page_size: int = Field(default=20, ge=1, le=500, validation_alias=AliasChoices("page_size", "pageSize")) + + +class MermaidDiagramCreateRequest(MermaidRequestModel): + diagram_name: str = Field(min_length=1, max_length=255, validation_alias=AliasChoices("diagram_name", "diagramName")) + description: str | None = Field(default="", max_length=20000) + diagram_data: str | None = Field(default="", max_length=200000, validation_alias=AliasChoices("diagram_data", "diagramData")) + group: str | None = Field(default=None, max_length=128) + tags: list[str] = Field(default_factory=list) + + +class MermaidDiagramUpdateRequest(MermaidRequestModel): + id: str = Field(min_length=1, max_length=32) + diagram_name: str | None = Field(default=None, min_length=1, max_length=255, validation_alias=AliasChoices("diagram_name", "diagramName")) + description: str | None = Field(default=None, max_length=20000) + diagram_data: str | None = Field(default=None, max_length=200000, validation_alias=AliasChoices("diagram_data", "diagramData")) + group: str | None = Field(default=None, max_length=128) + tags: list[str] | None = None + + +class MermaidDiagramDataPatchRequest(MermaidRequestModel): + diagram_data: str = Field(min_length=1, max_length=200000, validation_alias=AliasChoices("diagram_data", "diagramData")) + + +class MermaidChatMessage(BaseModel): + role: Literal["user", "assistant"] + content: str = Field(min_length=1, max_length=20000) + + +class MermaidChatStreamRequest(MermaidRequestModel): + model_name: str | None = Field(default=None, max_length=128, validation_alias=AliasChoices("model_name", "modelName")) + diagram_data: str | None = Field(default=None, max_length=200000, validation_alias=AliasChoices("diagram_data", "diagramData")) + messages: list[MermaidChatMessage] = Field(default_factory=list) diff --git a/api/app/schemas/mind_map.py b/api/app/schemas/mind_map.py new file mode 100644 index 0000000..323e71d --- /dev/null +++ b/api/app/schemas/mind_map.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel, Field + + +class MindMapSummary(BaseModel): + id: str + map_name: str + descr: str | None = None + map_data: str | None = None + create_date: datetime + create_user: str | None = None + update_date: datetime + update_user: str | None = None + + +class MindMapPageResponse(BaseModel): + items: list[MindMapSummary] + total: int + page_num: int + page_size: int + + +class MindMapQueryRequest(BaseModel): + map_name: str | None = Field(default=None, max_length=255) + page_num: int = Field(default=0, ge=0) + page_size: int = Field(default=20, ge=1, le=200) + + +class MindMapCreateRequest(BaseModel): + map_name: str = Field(min_length=1, max_length=255) + descr: str | None = Field(default="", max_length=20000) + map_data: str | None = Field(default=None) + + +class MindMapBasicInfoUpdateRequest(BaseModel): + id: str = Field(min_length=1, max_length=32) + map_name: str = Field(min_length=1, max_length=255) + descr: str | None = Field(default="", max_length=20000) + + +class MindMapDataUpdateRequest(BaseModel): + id: str = Field(min_length=1, max_length=32) + map_data: str diff --git a/api/app/schemas/requirement.py b/api/app/schemas/requirement.py index 5d67968..e80237d 100644 --- a/api/app/schemas/requirement.py +++ b/api/app/schemas/requirement.py @@ -9,13 +9,15 @@ from .user import UserPublic RequirementStatus = Literal[ "PENDING_ANALYSIS", + "PENDING_REVIEW", "PENDING_REVISION", "OPEN", "IN_PROGRESS", "COMPLETED", + "CLOSED", "CANCELLED", ] -RequirementPriority = Literal["low", "medium", "high", "urgent"] +RequirementPriority = Literal["low", "medium", "high", "urgent", "LOW", "MEDIUM", "HIGH"] RequirementCommentKind = Literal["comment", "analysis", "revision", "system"] @@ -36,6 +38,10 @@ class RequirementSummary(BaseModel): closed_at: datetime | None = None created_at: datetime updated_at: datetime + result_msg: str | None = None + progress_percent: int = 0 + git_url: str | None = None + branch: str | None = None creator: UserPublic | None = None assignee: UserPublic | None = None reviewer: UserPublic | None = None @@ -94,7 +100,7 @@ class RequirementCommentPublic(BaseModel): class RequirementEventPublic(BaseModel): - id: int + id: str requirement_id: str actor_user_id: str | None = None event_type: str diff --git a/api/app/schemas/todo.py b/api/app/schemas/todo.py index b479e6c..751b262 100644 --- a/api/app/schemas/todo.py +++ b/api/app/schemas/todo.py @@ -5,26 +5,24 @@ from typing import Literal from pydantic import BaseModel, Field -from .user import UserPublic - -TodoStatus = Literal["TODO", "IN_PROGRESS", "DONE"] -TodoPriority = Literal["low", "medium", "high", "urgent"] +TodoStatus = Literal["SCHEDULED", "IN_PROGRESS", "COMPLETED", "CANCELLED", "EXPIRED"] +TodoPriority = Literal["LOW", "MEDIUM", "HIGH"] class TodoSummary(BaseModel): id: str title: str - description: str + descr: str | None = None status: TodoStatus priority: TodoPriority - assignee_user_id: str | None = None - creator_user_id: str | None = None - due_at: datetime | None = None - completed_at: datetime | None = None - created_at: datetime - updated_at: datetime - creator: UserPublic | None = None - assignee: UserPublic | None = None + start_time: datetime | None = None + due_date: datetime | None = None + expire_time: datetime | None = None + calendar_event_id: str | None = None + create_date: datetime + create_user: str | None = None + update_date: datetime + update_user: str | None = None class TodoListResponse(BaseModel): @@ -33,22 +31,37 @@ class TodoListResponse(BaseModel): class TodoCreateRequest(BaseModel): - title: str = Field(min_length=2, max_length=200) - description: str = Field(default="", max_length=20000) - status: TodoStatus = "TODO" - priority: TodoPriority = "medium" - assignee_user_id: str | None = Field(default=None, max_length=36) - due_at: datetime | None = None + title: str = Field(min_length=1, max_length=256) + descr: str = Field(default="", max_length=20000) + status: TodoStatus = "SCHEDULED" + priority: TodoPriority = "MEDIUM" + start_time: datetime | None = None + due_date: datetime | None = None + expire_time: datetime | None = None + is_sync: bool = False + calendar_event_id: str | None = Field(default=None, max_length=32) class TodoUpdateRequest(BaseModel): - title: str | None = Field(default=None, min_length=2, max_length=200) - description: str | None = Field(default=None, max_length=20000) + title: str | None = Field(default=None, min_length=1, max_length=256) + descr: str | None = Field(default=None, max_length=20000) + status: TodoStatus | None = None priority: TodoPriority | None = None - assignee_user_id: str | None = Field(default=None, max_length=36) - due_at: datetime | None = None + start_time: datetime | None = None + due_date: datetime | None = None + expire_time: datetime | None = None + calendar_event_id: str | None = Field(default=None, max_length=32) + is_sync: bool = False class TodoTransitionRequest(BaseModel): status: TodoStatus note: str | None = Field(default=None, max_length=2000) + is_sync: bool = False + + +class TodoMindMapInitResponse(BaseModel): + id: str + map_name: str + descr: str | None = None + map_data: str diff --git a/api/app/services/calendar_event_service.py b/api/app/services/calendar_event_service.py new file mode 100644 index 0000000..b95afa1 --- /dev/null +++ b/api/app/services/calendar_event_service.py @@ -0,0 +1,602 @@ +from __future__ import annotations + +import json +import logging +from collections.abc import AsyncGenerator +from datetime import datetime, timedelta + +from fastapi import HTTPException, status +from sqlalchemy import and_, func, select +from sqlalchemy.orm import Session + +from ..models.base import utcnow +from ..models.calendar_event import CalendarEvent +from ..models.todo import Todo +from ..models.user import User +from ..schemas.calendar_event import ( + CalendarEventCreateRequest, + CalendarEventPageResponse, + CalendarEventQueryRequest, + CalendarEventSummary, + CalendarEventUpdateRequest, +) +from .llm_gateway import create_assistant_reply + +logger = logging.getLogger(__name__) + +SCHEDULE_ACTIVE_STATUSES = {"SCHEDULED", "IN_PROGRESS"} + +SCHEDULE_GENERATION_PROMPT = """你是日程生成助手。\n请根据用户输入,输出一个 JSON 对象,不要输出额外文本。\n字段要求:\n- title: string,日程标题\n- descr: string,日程描述\n- status: 固定返回 SCHEDULED\n- priority: LOW | MEDIUM | HIGH\n- start_time: ISO-8601 日期时间字符串\n- end_time: ISO-8601 日期时间字符串\n- expire_time: ISO-8601 日期时间字符串或 null\n- all_day: boolean\n\n如果用户没有提供明确时间,start_time 使用当前时间后 1 小时,end_time 为 start_time 后 1 小时。\n""" + + +def search_calendar_events( + db: Session, + payload: CalendarEventQueryRequest, + *, + actor: User, +) -> CalendarEventPageResponse: + _expire_overdue_events(db) + + filters = [CalendarEvent.create_user == actor.username] + if payload.title: + keyword = f"%{payload.title.strip()}%" + filters.append(CalendarEvent.title.ilike(keyword)) + if payload.status: + filters.append(CalendarEvent.status == payload.status) + if payload.priority: + filters.append(CalendarEvent.priority == payload.priority) + if payload.start_time_from: + filters.append(CalendarEvent.start_time >= payload.start_time_from) + if payload.start_time_to: + filters.append(CalendarEvent.start_time <= payload.start_time_to) + + where_clause = and_(*filters) + + total = int( + db.scalar( + select(func.count()) + .select_from(CalendarEvent) + .where(where_clause) + ) + or 0 + ) + + events = db.execute( + select(CalendarEvent) + .where(where_clause) + .order_by(CalendarEvent.create_date.desc()) + .offset(payload.page_num * payload.page_size) + .limit(payload.page_size) + ).scalars().all() + + return CalendarEventPageResponse( + items=[serialize_calendar_event(item) for item in events], + total=total, + page_num=payload.page_num, + page_size=payload.page_size, + ) + + +def get_calendar_event_by_id( + db: Session, + event_id: str, + *, + actor: User | None = None, +) -> CalendarEvent | None: + event = db.execute(select(CalendarEvent).where(CalendarEvent.id == event_id)).scalar_one_or_none() + if not event: + return None + if actor and event.create_user != actor.username: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to access calendar event") + return event + + +def get_calendar_event_by_todo_id(db: Session, todo_id: str) -> CalendarEvent | None: + return db.execute( + select(CalendarEvent).where(CalendarEvent.todo_id == todo_id) + ).scalar_one_or_none() + + +def create_calendar_event( + db: Session, + payload: CalendarEventCreateRequest, + *, + actor: User, + syncing: bool = False, +) -> CalendarEventSummary: + if payload.end_time <= payload.start_time: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="end_time must be later than start_time") + + now = utcnow() + event = CalendarEvent( + title=payload.title.strip(), + descr=payload.descr.strip(), + status=payload.status, + priority=payload.priority, + start_time=payload.start_time, + end_time=payload.end_time, + expire_time=payload.expire_time, + all_day=payload.all_day, + completed_at=now if payload.status == "COMPLETED" else None, + todo_id=payload.todo_id, + create_user=actor.username, + update_user=actor.username, + create_date=now, + update_date=now, + ) + db.add(event) + db.commit() + db.refresh(event) + + if not (payload.is_sync or syncing): + _sync_create_todo_for_event(db, event=event, actor=actor) + + saved = get_calendar_event_by_id(db, event.id) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Calendar event save failed") + return serialize_calendar_event(saved) + + +def update_calendar_event( + db: Session, + payload: CalendarEventUpdateRequest, + *, + actor: User, + syncing: bool = False, +) -> CalendarEventSummary: + event = get_calendar_event_by_id(db, payload.id, actor=actor) + if not event: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Calendar event not found") + + update_data = payload.model_dump(exclude_unset=True) + + next_start = update_data.get("start_time", event.start_time) + next_end = update_data.get("end_time", event.end_time) + if next_end <= next_start: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="end_time must be later than start_time") + + for field in [ + "title", + "descr", + "status", + "priority", + "start_time", + "end_time", + "expire_time", + "all_day", + "completed_at", + ]: + if field in update_data: + value = update_data[field] + if isinstance(value, str): + value = value.strip() + setattr(event, field, value) + + if "status" in update_data: + if event.status == "COMPLETED" and event.completed_at is None: + event.completed_at = utcnow() + if event.status != "COMPLETED" and "completed_at" not in update_data: + event.completed_at = None + + event.update_user = actor.username + event.update_date = utcnow() + db.commit() + db.refresh(event) + + if not (payload.is_sync or syncing): + _sync_update_todo_for_event(db, event=event, actor=actor) + + saved = get_calendar_event_by_id(db, event.id) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Calendar event load failed") + return serialize_calendar_event(saved) + + +def delete_calendar_event( + db: Session, + event_id: str, + *, + actor: User, + syncing: bool = False, +) -> dict[str, bool]: + event = get_calendar_event_by_id(db, event_id, actor=actor) + if not event: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Calendar event not found") + + linked_todo_id = event.todo_id + db.delete(event) + db.commit() + + if linked_todo_id and not syncing: + from .todo_service import delete_todo + + try: + delete_todo(db, linked_todo_id, actor=actor, syncing=True) + except HTTPException as exc: + if exc.status_code != status.HTTP_404_NOT_FOUND: + raise + + return {"success": True} + + +def complete_calendar_event( + db: Session, + event_id: str, + *, + actor: User, + syncing: bool = False, +) -> CalendarEventSummary: + event = get_calendar_event_by_id(db, event_id, actor=actor) + if not event: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Calendar event not found") + + event.status = "COMPLETED" + event.completed_at = utcnow() + event.update_user = actor.username + event.update_date = utcnow() + db.commit() + db.refresh(event) + + if event.todo_id and not syncing: + from ..schemas.todo import TodoTransitionRequest + from .todo_service import transition_todo + + try: + transition_todo( + db, + event.todo_id, + TodoTransitionRequest(status="COMPLETED", is_sync=True), + actor=actor, + syncing=True, + ) + except HTTPException as exc: + if exc.status_code != status.HTTP_404_NOT_FOUND: + raise + + saved = get_calendar_event_by_id(db, event.id) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Calendar event load failed") + return serialize_calendar_event(saved) + + +async def stream_generate_calendar_event( + db: Session, + *, + descr: str, +) -> AsyncGenerator[str, None]: + text = descr.strip() + if not text: + yield "[ERROR]日程描述不能为空" + return + + yield "connected" + + try: + result = create_assistant_reply( + db, + user_message=text, + context_messages=[], + system_prompt=SCHEDULE_GENERATION_PROMPT, + ) + content = result.content.strip() + except HTTPException as exc: + yield f"[ERROR]{exc.detail}" + return + except Exception as exc: # pragma: no cover - defensive fallback + yield f"[ERROR]服务异常: {exc}" + return + + for chunk in _chunk_text(content, chunk_size=120): + yield chunk + + try: + generated = _coerce_generated_event(content) + yield "[PARSE_RESULT]" + yield f"[EVENT]{json.dumps(generated, ensure_ascii=False)}" + except Exception as exc: + yield f"[ERROR]解析JSON失败: {exc}" + + +def serialize_calendar_event(event: CalendarEvent) -> CalendarEventSummary: + return CalendarEventSummary( + id=event.id, + title=event.title, + descr=event.descr, + status=event.status, + priority=event.priority, + start_time=event.start_time, + end_time=event.end_time, + expire_time=event.expire_time, + all_day=bool(event.all_day), + completed_at=event.completed_at, + todo_id=event.todo_id, + create_date=event.create_date, + create_user=event.create_user, + update_date=event.update_date, + update_user=event.update_user, + ) + + +def sync_from_todo_create(db: Session, *, todo: Todo, actor: User) -> None: + if todo.calendar_event_id: + return + + start_time = todo.start_time or todo.due_date or utcnow() + end_time = todo.due_date or (start_time + timedelta(hours=1)) + + try: + event = create_calendar_event( + db, + CalendarEventCreateRequest( + title=todo.title, + descr=todo.descr or "", + status=todo.status, + priority=todo.priority, + start_time=start_time, + end_time=end_time, + expire_time=todo.expire_time, + all_day=False, + is_sync=True, + todo_id=todo.id, + ), + actor=actor, + syncing=True, + ) + except Exception as exc: # pragma: no cover - best effort sync + logger.warning("Failed to sync todo->schedule create: %s", exc) + return + + todo.calendar_event_id = event.id + todo.update_user = actor.username + todo.update_date = utcnow() + db.commit() + + +def sync_from_todo_update(db: Session, *, todo: Todo, actor: User) -> None: + event_id = todo.calendar_event_id + if not event_id: + sync_from_todo_create(db, todo=todo, actor=actor) + return + + event = get_calendar_event_by_id(db, event_id) + if not event: + sync_from_todo_create(db, todo=todo, actor=actor) + return + + next_start = todo.start_time or event.start_time + next_end = todo.due_date or event.end_time + if next_end <= next_start: + next_end = next_start + timedelta(hours=1) + + try: + update_calendar_event( + db, + CalendarEventUpdateRequest( + id=event.id, + title=todo.title, + descr=todo.descr or "", + status=todo.status, + priority=todo.priority, + start_time=next_start, + end_time=next_end, + expire_time=todo.expire_time, + is_sync=True, + ), + actor=actor, + syncing=True, + ) + except Exception as exc: # pragma: no cover - best effort sync + logger.warning("Failed to sync todo->schedule update: %s", exc) + + +def sync_from_todo_delete(db: Session, *, todo: Todo, actor: User) -> None: + event_id = todo.calendar_event_id + if not event_id: + event = get_calendar_event_by_todo_id(db, todo.id) + event_id = event.id if event else None + + if not event_id: + return + + try: + delete_calendar_event(db, event_id, actor=actor, syncing=True) + except Exception as exc: # pragma: no cover - best effort sync + logger.warning("Failed to sync todo->schedule delete: %s", exc) + + +def sync_from_todo_transition(db: Session, *, todo: Todo, actor: User) -> None: + if not todo.calendar_event_id: + return + + event = get_calendar_event_by_id(db, todo.calendar_event_id) + if not event: + return + + try: + payload = CalendarEventUpdateRequest( + id=event.id, + status=todo.status, + is_sync=True, + ) + if todo.status == "COMPLETED": + payload.completed_at = utcnow() + update_calendar_event(db, payload, actor=actor, syncing=True) + except Exception as exc: # pragma: no cover - best effort sync + logger.warning("Failed to sync todo->schedule transition: %s", exc) + + +def _sync_create_todo_for_event(db: Session, *, event: CalendarEvent, actor: User) -> None: + from ..schemas.todo import TodoCreateRequest + from .todo_service import create_todo + + try: + todo = create_todo( + db, + TodoCreateRequest( + title=event.title, + descr=event.descr, + status=event.status, + priority=event.priority, + start_time=event.start_time, + due_date=event.end_time, + expire_time=event.expire_time, + is_sync=True, + calendar_event_id=event.id, + ), + actor=actor, + syncing=True, + ) + except Exception as exc: # pragma: no cover - best effort sync + logger.warning("Failed to sync schedule->todo create: %s", exc) + return + + event.todo_id = todo.id + event.update_user = actor.username + event.update_date = utcnow() + db.commit() + + +def _sync_update_todo_for_event(db: Session, *, event: CalendarEvent, actor: User) -> None: + if not event.todo_id: + return + + from ..schemas.todo import TodoTransitionRequest, TodoUpdateRequest + from .todo_service import get_todo_by_id, transition_todo, update_todo + + todo = get_todo_by_id(db, event.todo_id) + if not todo: + return + + try: + update_todo( + db, + event.todo_id, + TodoUpdateRequest( + title=event.title, + descr=event.descr, + priority=event.priority, + start_time=event.start_time, + due_date=event.end_time, + expire_time=event.expire_time, + calendar_event_id=event.id, + is_sync=True, + ), + actor=actor, + syncing=True, + ) + + if todo.status != event.status: + transition_todo( + db, + event.todo_id, + TodoTransitionRequest(status=event.status, is_sync=True), + actor=actor, + syncing=True, + ) + except Exception as exc: # pragma: no cover - best effort sync + logger.warning("Failed to sync schedule->todo update: %s", exc) + + +def _expire_overdue_events(db: Session) -> int: + now = utcnow() + events = db.execute( + select(CalendarEvent).where( + CalendarEvent.expire_time.is_not(None), + CalendarEvent.expire_time <= now, + CalendarEvent.status.in_(sorted(SCHEDULE_ACTIVE_STATUSES)), + ) + ).scalars().all() + + if not events: + return 0 + + for event in events: + event.status = "EXPIRED" + event.update_user = "system" + event.update_date = now + + db.commit() + return len(events) + + +def _chunk_text(text: str, *, chunk_size: int) -> list[str]: + if not text: + return [] + return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)] + + +def _coerce_generated_event(content: str) -> dict[str, object]: + payload = _extract_json_object(content) + + title = str(payload.get("title") or "新日程").strip() or "新日程" + descr = str(payload.get("descr") or payload.get("description") or "").strip() + + start_value = payload.get("start_time") or payload.get("startTime") + end_value = payload.get("end_time") or payload.get("endTime") + expire_value = payload.get("expire_time") or payload.get("expireTime") + + start_dt = _parse_datetime(start_value) or (utcnow() + timedelta(hours=1)) + end_dt = _parse_datetime(end_value) or (start_dt + timedelta(hours=1)) + if end_dt <= start_dt: + end_dt = start_dt + timedelta(hours=1) + + expire_dt = _parse_datetime(expire_value) + + status_value = str(payload.get("status") or "SCHEDULED").upper() + if status_value not in {"SCHEDULED", "IN_PROGRESS", "COMPLETED", "CANCELLED", "EXPIRED"}: + status_value = "SCHEDULED" + + priority_value = str(payload.get("priority") or "MEDIUM").upper() + if priority_value not in {"LOW", "MEDIUM", "HIGH"}: + priority_value = "MEDIUM" + + all_day = bool(payload.get("all_day") if "all_day" in payload else payload.get("allDay", False)) + + return { + "title": title, + "descr": descr, + "status": status_value, + "priority": priority_value, + "start_time": start_dt.isoformat(), + "end_time": end_dt.isoformat(), + "expire_time": expire_dt.isoformat() if expire_dt else None, + "all_day": all_day, + } + + +def _extract_json_object(content: str) -> dict[str, object]: + text = content.strip() + try: + value = json.loads(text) + if isinstance(value, dict): + return value + except json.JSONDecodeError: + pass + + start = text.find("{") + end = text.rfind("}") + if start == -1 or end == -1 or end <= start: + raise ValueError("no json object found") + + candidate = text[start:end + 1] + value = json.loads(candidate) + if not isinstance(value, dict): + raise ValueError("json payload must be object") + return value + + +def _parse_datetime(value: object) -> datetime | None: + if value is None: + return None + if isinstance(value, datetime): + return value + if isinstance(value, str): + text = value.strip() + if not text: + return None + normalized = text.replace("Z", "+00:00") + try: + return datetime.fromisoformat(normalized) + except ValueError: + return None + return None diff --git a/api/app/services/diary_service.py b/api/app/services/diary_service.py new file mode 100644 index 0000000..965d318 --- /dev/null +++ b/api/app/services/diary_service.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +from datetime import date + +from fastapi import HTTPException, status +from sqlalchemy import func, select +from sqlalchemy.orm import Session + +from ..models.base import utcnow +from ..models.diary import Diary +from ..models.user import User +from ..schemas.diary import ( + DiaryCreateRequest, + DiaryPageResponse, + DiaryQueryRequest, + DiarySummary, + DiaryUpdateRequest, +) + + +def serialize_diary(item: Diary) -> DiarySummary: + return DiarySummary( + id=item.id, + title=item.title, + content=item.content, + diary_date=item.diary_date, + mood=item.mood, + weather=item.weather, + archived=bool(item.archived), + create_date=item.create_date, + create_user=item.create_user, + update_date=item.update_date, + update_user=item.update_user, + ) + + +def get_diary_by_id( + db: Session, + diary_id: str, + *, + actor: User | None = None, +) -> Diary | None: + item = db.execute(select(Diary).where(Diary.id == diary_id)).scalar_one_or_none() + if not item: + return None + if actor and item.create_user and item.create_user != actor.username: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to access diary") + return item + + +def search_diaries( + db: Session, + payload: DiaryQueryRequest, + *, + actor: User, +) -> DiaryPageResponse: + filters = [Diary.create_user == actor.username] + + title = (payload.title or "").strip() + if title: + filters.append(Diary.title.ilike(f"%{title}%")) + if payload.mood: + filters.append(Diary.mood == payload.mood) + if payload.archived is not None: + filters.append(Diary.archived == payload.archived) + if payload.diary_date_start: + filters.append(Diary.diary_date >= payload.diary_date_start) + if payload.diary_date_end: + filters.append(Diary.diary_date <= payload.diary_date_end) + + total_stmt = select(func.count()).select_from(Diary).where(*filters) + total = int(db.execute(total_stmt).scalar_one() or 0) + + items = db.execute( + select(Diary) + .where(*filters) + .order_by(Diary.diary_date.desc(), Diary.create_date.desc()) + .offset(payload.page_num * payload.page_size) + .limit(payload.page_size) + ).scalars().all() + + return DiaryPageResponse( + items=[serialize_diary(item) for item in items], + total=total, + page_num=payload.page_num, + page_size=payload.page_size, + ) + + +def create_diary( + db: Session, + payload: DiaryCreateRequest, + *, + actor: User, +) -> DiarySummary: + now = utcnow() + item = Diary( + title=payload.title.strip(), + content=payload.content.strip(), + diary_date=payload.diary_date or date.today(), + mood=payload.mood or "CALM", + weather=_normalize_nullable_text(payload.weather), + archived=bool(payload.archived), + create_user=actor.username, + update_user=actor.username, + create_date=now, + update_date=now, + ) + db.add(item) + db.commit() + + saved = get_diary_by_id(db, item.id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Diary save failed") + return serialize_diary(saved) + + +def update_diary( + db: Session, + payload: DiaryUpdateRequest, + *, + actor: User, +) -> DiarySummary: + item = get_diary_by_id(db, payload.id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Diary not found") + + item.title = payload.title.strip() + item.content = payload.content.strip() + item.diary_date = payload.diary_date + item.mood = payload.mood + item.weather = _normalize_nullable_text(payload.weather) + item.archived = bool(payload.archived) + item.update_user = actor.username + item.update_date = utcnow() + + db.commit() + + saved = get_diary_by_id(db, payload.id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Diary load failed") + return serialize_diary(saved) + + +def delete_diary( + db: Session, + diary_id: str, + *, + actor: User, +) -> dict[str, bool]: + item = get_diary_by_id(db, diary_id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Diary not found") + + db.delete(item) + db.commit() + return {"success": True} + + +def archive_diary( + db: Session, + diary_id: str, + archived: bool, + *, + actor: User, +) -> DiarySummary: + item = get_diary_by_id(db, diary_id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Diary not found") + + item.archived = bool(archived) + item.update_user = actor.username + item.update_date = utcnow() + db.commit() + + saved = get_diary_by_id(db, diary_id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Diary load failed") + return serialize_diary(saved) + + +def _normalize_nullable_text(value: str | None) -> str | None: + if value is None: + return None + text = value.strip() + return text or None diff --git a/api/app/services/mermaid_service.py b/api/app/services/mermaid_service.py new file mode 100644 index 0000000..4c76763 --- /dev/null +++ b/api/app/services/mermaid_service.py @@ -0,0 +1,525 @@ +from __future__ import annotations + +from collections.abc import AsyncGenerator +from datetime import datetime +from uuid import uuid4 + +from fastapi import HTTPException, status +from sqlalchemy import delete, func, select +from sqlalchemy.orm import Session + +from ..models.base import utcnow +from ..models.mermaid_diagram import MermaidDiagram +from ..models.model_registry import ModelApiKey, ModelRegistry +from ..models.object_group import ObjectGroup, ObjectGroupRelation +from ..models.user import User +from ..schemas.mermaid import ( + MermaidChatStreamRequest, + MermaidDiagramCreateRequest, + MermaidDiagramDataPatchRequest, + MermaidDiagramPageResponse, + MermaidDiagramQueryRequest, + MermaidDiagramSummary, + MermaidDiagramUpdateRequest, + MermaidGroupListResponse, + MermaidGroupSummary, +) +from .llm_gateway import create_assistant_reply, create_reply_with_model + +MERMAID_GROUP_TYPE = "MERMAID" + +MERMAID_GENERATE_SYSTEM_PROMPT = """You are an expert in Mermaid diagrams. +Please update or generate Mermaid code based on the user advice. +IMPORTANT RULES: +1. Return ONLY the raw Mermaid code. +2. Do NOT wrap the code in markdown code blocks. +3. Do NOT include any conversational text.""" + +MERMAID_CHAT_SYSTEM_PROMPT_TEMPLATE = """You are an expert in Mermaid diagrams. +Current Mermaid Code: +{diagram_data} + +IMPORTANT RULES: +1. Return ONLY the raw Mermaid code. +2. Do NOT wrap the code in markdown code blocks. +3. Do NOT include any conversational text.""" + + +def search_mermaid_diagrams( + db: Session, + payload: MermaidDiagramQueryRequest, + *, + actor: User, +) -> MermaidDiagramPageResponse: + filters = [MermaidDiagram.create_user == actor.username] + + keyword = _normalize_str(payload.key_word) + if keyword: + filters.append(MermaidDiagram.diagram_name.ilike(f"%{keyword}%")) + + normalized_group = _normalize_str(payload.group) + if normalized_group: + group_obj_ids = db.execute( + select(ObjectGroupRelation.obj_id) + .join(ObjectGroup, ObjectGroup.id == ObjectGroupRelation.group_id) + .where( + ObjectGroup.create_user == actor.username, + ObjectGroup.type == MERMAID_GROUP_TYPE, + ObjectGroup.name == normalized_group, + ) + ).scalars().all() + if not group_obj_ids: + return MermaidDiagramPageResponse( + items=[], + total=0, + page_num=payload.page_num, + page_size=payload.page_size, + ) + filters.append(MermaidDiagram.id.in_(set(group_obj_ids))) + + total_stmt = select(func.count()).select_from(MermaidDiagram).where(*filters) + total = int(db.execute(total_stmt).scalar_one() or 0) + + items = db.execute( + select(MermaidDiagram) + .where(*filters) + .order_by(MermaidDiagram.update_date.desc(), MermaidDiagram.create_date.desc()) + .offset(payload.page_num * payload.page_size) + .limit(payload.page_size) + ).scalars().all() + + group_map = _build_group_map(db, [item.id for item in items]) + return MermaidDiagramPageResponse( + items=[serialize_mermaid_diagram(item, group_map.get(item.id)) for item in items], + total=total, + page_num=payload.page_num, + page_size=payload.page_size, + ) + + +def list_mermaid_groups( + db: Session, + *, + actor: User, +) -> MermaidGroupListResponse: + groups = db.execute( + select(ObjectGroup) + .where( + ObjectGroup.create_user == actor.username, + ObjectGroup.type == MERMAID_GROUP_TYPE, + ) + .order_by(ObjectGroup.label.asc(), ObjectGroup.name.asc()) + ).scalars().all() + return MermaidGroupListResponse( + items=[serialize_mermaid_group(item) for item in groups], + total=len(groups), + ) + + +def get_mermaid_diagram_by_id( + db: Session, + diagram_id: str, + *, + actor: User | None = None, +) -> MermaidDiagram | None: + item = db.execute(select(MermaidDiagram).where(MermaidDiagram.id == diagram_id)).scalar_one_or_none() + if not item: + return None + if actor and item.create_user != actor.username: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to access mermaid diagram") + return item + + +def get_mermaid_diagram_summary( + db: Session, + diagram_id: str, + *, + actor: User, +) -> MermaidDiagramSummary | None: + item = get_mermaid_diagram_by_id(db, diagram_id, actor=actor) + if not item: + return None + group_map = _build_group_map(db, [item.id]) + return serialize_mermaid_diagram(item, group_map.get(item.id)) + + +def create_mermaid_diagram( + db: Session, + payload: MermaidDiagramCreateRequest, + *, + actor: User, +) -> MermaidDiagramSummary: + now = utcnow() + item = MermaidDiagram( + id=uuid4().hex, + diagram_name=payload.diagram_name.strip(), + description=_normalize_str(payload.description) or "", + diagram_data=_normalize_str(payload.diagram_data) or "", + create_user=actor.username, + update_user=actor.username, + create_date=now, + update_date=now, + ) + db.add(item) + db.flush() + + _replace_mermaid_group_relation( + db, + diagram_id=item.id, + group_name=payload.group, + actor=actor, + now=now, + ) + + db.commit() + saved = get_mermaid_diagram_summary(db, item.id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Mermaid diagram save failed") + return saved + + +def update_mermaid_diagram( + db: Session, + payload: MermaidDiagramUpdateRequest, + *, + actor: User, +) -> MermaidDiagramSummary: + item = get_mermaid_diagram_by_id(db, payload.id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mermaid diagram not found") + + now = utcnow() + update_data = payload.model_dump(exclude_unset=True) + + if "diagram_name" in update_data and update_data["diagram_name"] is not None: + item.diagram_name = str(update_data["diagram_name"]).strip() + if "description" in update_data: + item.description = _normalize_str(update_data["description"]) or "" + if "diagram_data" in update_data: + item.diagram_data = _normalize_str(update_data["diagram_data"]) or "" + + if "group" in update_data: + _replace_mermaid_group_relation( + db, + diagram_id=item.id, + group_name=update_data["group"], + actor=actor, + now=now, + ) + + item.update_user = actor.username + item.update_date = now + db.commit() + + saved = get_mermaid_diagram_summary(db, item.id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Mermaid diagram load failed") + return saved + + +def update_mermaid_diagram_data( + db: Session, + diagram_id: str, + payload: MermaidDiagramDataPatchRequest, + *, + actor: User, +) -> MermaidDiagramSummary: + item = get_mermaid_diagram_by_id(db, diagram_id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mermaid diagram not found") + + item.diagram_data = payload.diagram_data.strip() + item.update_user = actor.username + item.update_date = utcnow() + db.commit() + + saved = get_mermaid_diagram_summary(db, diagram_id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Mermaid diagram load failed") + return saved + + +def delete_mermaid_diagram( + db: Session, + diagram_id: str, + *, + actor: User, +) -> dict[str, bool]: + item = get_mermaid_diagram_by_id(db, diagram_id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mermaid diagram not found") + + db.execute(delete(ObjectGroupRelation).where(ObjectGroupRelation.obj_id == item.id)) + db.delete(item) + db.commit() + return {"success": True} + + +async def stream_generate_mermaid_code( + db: Session, + *, + advice: str, + diagram_data: str | None, + model_name: str | None, +) -> AsyncGenerator[str, None]: + normalized_advice = (advice or "").strip() + if not normalized_advice: + yield "[ERROR]Advice cannot be empty" + return + + system_prompt = ( + f"{MERMAID_GENERATE_SYSTEM_PROMPT}\n\n" + f"Current Mermaid Code:\n{(diagram_data or '').strip()}" + ) + + try: + result = _generate_mermaid_reply( + db=db, + user_message=normalized_advice, + context_messages=[], + system_prompt=system_prompt, + model_name=model_name, + ) + except HTTPException as exc: + yield f"[ERROR]{exc.detail}" + return + except Exception as exc: # pragma: no cover - defensive fallback + yield f"[ERROR]服务异常: {exc}" + return + + for chunk in _chunk_text(result, chunk_size=120): + yield chunk + + +async def stream_chat_mermaid_code( + db: Session, + payload: MermaidChatStreamRequest, +) -> AsyncGenerator[str, None]: + normalized_messages: list[tuple[str, str]] = [] + for item in payload.messages: + content = item.content.strip() + if not content: + continue + normalized_messages.append((item.role, content)) + + if not normalized_messages: + yield "[ERROR]Messages cannot be empty" + return + + user_message: str | None = None + context_messages: list[tuple[str, str]] = [] + for role, content in normalized_messages: + if role == "user": + user_message = content + continue + if role in {"assistant"}: + context_messages.append((role, content)) + + if not user_message: + yield "[ERROR]Last user message is required" + return + + # Use all messages except last user turn as context. + context_messages = [ + (role, content) + for role, content in normalized_messages[:-1] + if role in {"user", "assistant"} + ] + + system_prompt = MERMAID_CHAT_SYSTEM_PROMPT_TEMPLATE.format( + diagram_data=(payload.diagram_data or "").strip(), + ) + + try: + result = _generate_mermaid_reply( + db=db, + user_message=user_message, + context_messages=context_messages, + system_prompt=system_prompt, + model_name=payload.model_name, + ) + except HTTPException as exc: + yield f"[ERROR]{exc.detail}" + return + except Exception as exc: # pragma: no cover - defensive fallback + yield f"[ERROR]服务异常: {exc}" + return + + for chunk in _chunk_text(result, chunk_size=120): + yield chunk + + +def serialize_mermaid_group(item: ObjectGroup) -> MermaidGroupSummary: + return MermaidGroupSummary( + id=item.id, + name=item.name, + label=item.label, + type=item.type, + descr=item.descr, + ) + + +def serialize_mermaid_diagram( + item: MermaidDiagram, + group: ObjectGroup | None, +) -> MermaidDiagramSummary: + return MermaidDiagramSummary( + id=item.id, + diagram_name=item.diagram_name, + description=item.description, + diagram_data=item.diagram_data, + group_name=group.name if group else None, + group_label=group.label if group else None, + tag_names=[], + tag_labels=[], + create_date=item.create_date, + create_user=item.create_user, + update_date=item.update_date, + update_user=item.update_user, + ) + + +def _build_group_map(db: Session, diagram_ids: list[str]) -> dict[str, ObjectGroup]: + if not diagram_ids: + return {} + + relation_rows = db.execute( + select(ObjectGroupRelation.obj_id, ObjectGroupRelation.group_id) + .where(ObjectGroupRelation.obj_id.in_(diagram_ids)) + .order_by(ObjectGroupRelation.rela_id.asc()) + ).all() + if not relation_rows: + return {} + + obj_to_group_id: dict[str, str] = {} + for obj_id, group_id in relation_rows: + if obj_id not in obj_to_group_id: + obj_to_group_id[obj_id] = group_id + + group_ids = sorted(set(obj_to_group_id.values())) + groups = db.execute(select(ObjectGroup).where(ObjectGroup.id.in_(group_ids))).scalars().all() + group_map = {group.id: group for group in groups} + return {obj_id: group_map[group_id] for obj_id, group_id in obj_to_group_id.items() if group_id in group_map} + + +def _replace_mermaid_group_relation( + db: Session, + *, + diagram_id: str, + group_name: str | None, + actor: User, + now: datetime, +) -> None: + db.execute(delete(ObjectGroupRelation).where(ObjectGroupRelation.obj_id == diagram_id)) + normalized_group_name = _normalize_str(group_name) + if not normalized_group_name: + return + + group = db.execute( + select(ObjectGroup).where( + ObjectGroup.create_user == actor.username, + ObjectGroup.type == MERMAID_GROUP_TYPE, + ObjectGroup.name == normalized_group_name, + ) + ).scalar_one_or_none() + + if not group: + group = ObjectGroup( + id=uuid4().hex, + name=normalized_group_name, + label=normalized_group_name, + type=MERMAID_GROUP_TYPE, + descr="", + create_user=actor.username, + update_user=actor.username, + create_date=now, + update_date=now, + ) + db.add(group) + db.flush() + + relation = ObjectGroupRelation( + rela_id=uuid4().hex, + group_id=group.id, + obj_id=diagram_id, + ) + db.add(relation) + + +def _generate_mermaid_reply( + db: Session, + *, + user_message: str, + context_messages: list[tuple[str, str]], + system_prompt: str, + model_name: str | None, +) -> str: + model = _resolve_enabled_model_by_name(db, model_name=model_name) + if model: + result = create_reply_with_model( + model=model, + user_message=user_message, + context_messages=context_messages, + system_prompt=system_prompt, + ) + return result.content.strip() + + result = create_assistant_reply( + db, + user_message=user_message, + context_messages=context_messages, + system_prompt=system_prompt, + ) + return result.content.strip() + + +def _resolve_enabled_model_by_name( + db: Session, + *, + model_name: str | None, +) -> ModelRegistry | None: + normalized = _normalize_str(model_name) + if not normalized: + return None + + candidates = db.execute( + select(ModelRegistry) + .where( + ModelRegistry.name == normalized, + ModelRegistry.status == "ENABLED", + ) + .order_by(ModelRegistry.id.desc()) + ).scalars().all() + + for model in candidates: + active_key = db.scalar( + select(ModelApiKey.id).where( + ModelApiKey.model_id == model.id, + ModelApiKey.is_active.is_(True), + ) + ) + if active_key is not None: + return model + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Model not found or unavailable: {normalized}", + ) + + +def _normalize_str(value: str | None) -> str | None: + if value is None: + return None + normalized = str(value).strip() + return normalized or None + + +def _chunk_text(text: str, *, chunk_size: int) -> list[str]: + if not text: + return [] + chunks: list[str] = [] + start = 0 + while start < len(text): + chunks.append(text[start:start + chunk_size]) + start += chunk_size + return chunks diff --git a/api/app/services/mind_map_service.py b/api/app/services/mind_map_service.py new file mode 100644 index 0000000..f849f12 --- /dev/null +++ b/api/app/services/mind_map_service.py @@ -0,0 +1,373 @@ +from __future__ import annotations + +import json +from collections.abc import AsyncGenerator +from uuid import uuid4 + +from fastapi import HTTPException, status +from sqlalchemy import func, select +from sqlalchemy.orm import Session + +from ..models.base import utcnow +from ..models.mind_map import MindMap +from ..models.user import User +from ..schemas.mind_map import ( + MindMapBasicInfoUpdateRequest, + MindMapCreateRequest, + MindMapDataUpdateRequest, + MindMapPageResponse, + MindMapQueryRequest, + MindMapSummary, +) +from .llm_gateway import create_assistant_reply + +MIND_MAP_GENERATION_PROMPT = """你是思维导图生成助手。 +请根据用户描述,输出一个 JSON 对象,不要输出额外文字。 +JSON 结构必须符合下述格式: +{ + "nodeData": { "id": "root", "topic": "中心主题", "root": true }, + "nodeChild": [ + { + "nodeData": { "id": "n1", "topic": "一级主题" }, + "nodeChild": [ + { "nodeData": { "id": "n1-1", "topic": "二级主题" }, "nodeChild": [] } + ] + } + ] +} +要求: +1. 所有节点都使用 nodeData + nodeChild; +2. topic 用简洁中文短语; +3. 保持层次清晰,避免超过 4 层; +4. 输出必须是可解析 JSON。""" + + +def build_initial_mind_map_data(title: str) -> str: + topic = (title or "").strip() or "新思维导图" + data = { + "nodeData": { + "id": "root", + "topic": topic, + "root": True, + }, + "nodeChild": [], + } + return json.dumps(data, ensure_ascii=False) + + +def serialize_mind_map(item: MindMap) -> MindMapSummary: + return MindMapSummary( + id=item.id, + map_name=item.map_name, + descr=item.descr, + map_data=item.map_data, + create_date=item.create_date, + create_user=item.create_user, + update_date=item.update_date, + update_user=item.update_user, + ) + + +def search_mind_maps( + db: Session, + payload: MindMapQueryRequest, + *, + actor: User, +) -> MindMapPageResponse: + filters = [MindMap.create_user == actor.username] + keyword = (payload.map_name or "").strip() + if keyword: + filters.append(MindMap.map_name.ilike(f"%{keyword}%")) + + total_stmt = select(func.count()).select_from(MindMap).where(*filters) + total = int(db.execute(total_stmt).scalar_one() or 0) + + items = db.execute( + select(MindMap) + .where(*filters) + .order_by(MindMap.create_date.desc()) + .offset(payload.page_num * payload.page_size) + .limit(payload.page_size) + ).scalars().all() + + return MindMapPageResponse( + items=[serialize_mind_map(item) for item in items], + total=total, + page_num=payload.page_num, + page_size=payload.page_size, + ) + + +def get_mind_map_by_id( + db: Session, + mind_map_id: str, + *, + actor: User | None = None, +) -> MindMap | None: + item = db.execute(select(MindMap).where(MindMap.id == mind_map_id)).scalar_one_or_none() + if not item: + return None + if actor and item.create_user != actor.username: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to access mind map") + return item + + +def create_mind_map( + db: Session, + payload: MindMapCreateRequest, + *, + actor: User, + fixed_id: str | None = None, +) -> MindMapSummary: + now = utcnow() + map_name = payload.map_name.strip() + item = MindMap( + id=(fixed_id or uuid4().hex), + map_name=map_name, + descr=_normalize_str(payload.descr) or "", + map_data=_normalize_map_data(payload.map_data, map_name=map_name), + create_user=actor.username, + update_user=actor.username, + create_date=now, + update_date=now, + ) + db.add(item) + db.commit() + + saved = get_mind_map_by_id(db, item.id) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Mind map save failed") + return serialize_mind_map(saved) + + +def update_mind_map_basic_info( + db: Session, + payload: MindMapBasicInfoUpdateRequest, + *, + actor: User, +) -> MindMapSummary: + item = get_mind_map_by_id(db, payload.id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mind map not found") + + item.map_name = payload.map_name.strip() + item.descr = _normalize_str(payload.descr) or "" + item.update_user = actor.username + item.update_date = utcnow() + db.commit() + + saved = get_mind_map_by_id(db, payload.id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Mind map load failed") + return serialize_mind_map(saved) + + +def update_mind_map_data( + db: Session, + payload: MindMapDataUpdateRequest, + *, + actor: User, +) -> MindMapSummary: + item = get_mind_map_by_id(db, payload.id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mind map not found") + + item.map_data = _normalize_map_data(payload.map_data, map_name=item.map_name) + item.update_user = actor.username + item.update_date = utcnow() + db.commit() + + saved = get_mind_map_by_id(db, payload.id, actor=actor) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Mind map load failed") + return serialize_mind_map(saved) + + +def delete_mind_map( + db: Session, + mind_map_id: str, + *, + actor: User, +) -> dict[str, bool]: + item = get_mind_map_by_id(db, mind_map_id, actor=actor) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mind map not found") + + db.delete(item) + db.commit() + return {"success": True} + + +async def stream_generate_mind_map( + db: Session, + *, + descr: str, +) -> AsyncGenerator[str, None]: + text = descr.strip() + if not text: + yield "[ERROR]思维导图描述不能为空" + return + + yield "connected" + try: + result = create_assistant_reply( + db, + user_message=text, + context_messages=[], + system_prompt=MIND_MAP_GENERATION_PROMPT, + ) + content = result.content.strip() + except HTTPException as exc: + yield f"[ERROR]{exc.detail}" + return + except Exception as exc: # pragma: no cover - defensive fallback + yield f"[ERROR]服务异常: {exc}" + return + + for chunk in _chunk_text(content, chunk_size=120): + yield chunk + + try: + generated = _coerce_generated_mind_map(content) + yield "[PARSE_RESULT]" + yield f"[MINDMAP]{json.dumps(generated, ensure_ascii=False)}" + except Exception as exc: + yield f"[ERROR]解析JSON失败: {exc}" + + +def _normalize_map_data(map_data: str | None, *, map_name: str) -> str: + value = (map_data or "").strip() + if not value: + return build_initial_mind_map_data(map_name) + + try: + parsed = json.loads(value) + except json.JSONDecodeError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid map_data JSON: {exc}") from exc + + if not isinstance(parsed, dict): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="map_data must be a JSON object") + return json.dumps(parsed, ensure_ascii=False) + + +def _coerce_generated_mind_map(content: str) -> dict[str, object]: + parsed = _load_json_object(content) + if "nodeData" in parsed: + return _normalize_node_tree(parsed, is_root=True) + if "root" in parsed and isinstance(parsed["root"], dict): + return _normalize_root_tree(parsed["root"], is_root=True) + raise ValueError("JSON 格式不符合思维导图结构") + + +def _normalize_node_tree(node: dict[str, object], *, is_root: bool) -> dict[str, object]: + node_data_raw = node.get("nodeData") + node_data = node_data_raw if isinstance(node_data_raw, dict) else {} + topic = _pick_topic(node_data, fallback="中心主题" if is_root else "未命名主题") + node_id = _pick_node_id(node_data, fallback="root" if is_root else None) + + child_source = node.get("nodeChild") + if not isinstance(child_source, list): + alt_children = node.get("children") + child_source = alt_children if isinstance(alt_children, list) else [] + + normalized_children = [ + _normalize_node_tree(child, is_root=False) + for child in child_source + if isinstance(child, dict) + ] + normalized_node_data: dict[str, object] = { + "id": node_id, + "topic": topic, + } + if is_root: + normalized_node_data["root"] = True + + return { + "nodeData": normalized_node_data, + "nodeChild": normalized_children, + } + + +def _normalize_root_tree(node: dict[str, object], *, is_root: bool) -> dict[str, object]: + data = node.get("data") + data_obj = data if isinstance(data, dict) else {} + topic = _pick_topic(data_obj, fallback="中心主题" if is_root else "未命名主题") + node_id = _pick_node_id(node, fallback="root" if is_root else None) + + children_obj = node.get("children") + children = children_obj if isinstance(children_obj, list) else [] + + normalized_children = [ + _normalize_root_tree(child, is_root=False) + for child in children + if isinstance(child, dict) + ] + normalized_node_data: dict[str, object] = { + "id": node_id, + "topic": topic, + } + if is_root: + normalized_node_data["root"] = True + + return { + "nodeData": normalized_node_data, + "nodeChild": normalized_children, + } + + +def _pick_topic(data: dict[str, object], *, fallback: str) -> str: + for key in ("topic", "text", "name", "title"): + value = data.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + return fallback + + +def _pick_node_id(data: dict[str, object], *, fallback: str | None) -> str: + value = data.get("id") + if isinstance(value, str) and value.strip(): + return value.strip() + if fallback: + return fallback + return uuid4().hex[:8] + + +def _load_json_object(content: str) -> dict[str, object]: + text = content.strip() + text = _strip_markdown_fence(text) + try: + parsed = json.loads(text) + except json.JSONDecodeError: + start = text.find("{") + end = text.rfind("}") + if start < 0 or end <= start: + raise + parsed = json.loads(text[start : end + 1]) + + if not isinstance(parsed, dict): + raise ValueError("JSON 顶层必须是对象") + return parsed + + +def _strip_markdown_fence(content: str) -> str: + text = content.strip() + if text.startswith("```json"): + text = text[7:] + elif text.startswith("```"): + text = text[3:] + if text.endswith("```"): + text = text[:-3] + return text.strip() + + +def _chunk_text(text: str, *, chunk_size: int) -> list[str]: + if not text: + return [] + return [text[index : index + chunk_size] for index in range(0, len(text), chunk_size)] + + +def _normalize_str(value: str | None) -> str | None: + if value is None: + return None + normalized = value.strip() + return normalized or None diff --git a/api/app/services/requirement_service.py b/api/app/services/requirement_service.py index 0bc5f26..d1094ef 100644 --- a/api/app/services/requirement_service.py +++ b/api/app/services/requirement_service.py @@ -1,14 +1,14 @@ from __future__ import annotations import asyncio -from datetime import datetime +import math from fastapi import HTTPException, status -from sqlalchemy import or_, select +from sqlalchemy import func, or_, select from sqlalchemy.orm import Session, selectinload -from ..models.base import utcnow from ..models.requirement import Requirement, RequirementComment, RequirementEvent +from ..models.rbac import Role from ..models.user import User from ..schemas.requirement import ( RequirementAssignRequest, @@ -24,38 +24,38 @@ from ..schemas.requirement import ( from .push_service import publish_topic from .user_service import serialize_user -REQUIREMENT_LOAD_OPTIONS = ( - selectinload(Requirement.creator).selectinload(User.roles), - selectinload(Requirement.assignee).selectinload(User.roles), - selectinload(Requirement.reviewer).selectinload(User.roles), -) -COMMENT_LOAD_OPTIONS = ( - selectinload(RequirementComment.author).selectinload(User.roles), -) -EVENT_LOAD_OPTIONS = ( - selectinload(RequirementEvent.actor).selectinload(User.roles), -) TOPIC_NAME = "requirements" + +ZERO_PROGRESS_STATUSES = { + "PENDING_ANALYSIS", + "PENDING_REVIEW", + "PENDING_REVISION", + "OPEN", +} +VALID_STATUSES = { + "PENDING_ANALYSIS", + "PENDING_REVIEW", + "PENDING_REVISION", + "OPEN", + "IN_PROGRESS", + "COMPLETED", + "CLOSED", +} +STATUS_ALIASES = {"CANCELLED": "CLOSED"} ALLOWED_TRANSITIONS: dict[str, set[str]] = { - "PENDING_ANALYSIS": {"OPEN", "PENDING_REVISION", "CANCELLED"}, - "PENDING_REVISION": {"OPEN", "CANCELLED"}, - "OPEN": {"IN_PROGRESS", "PENDING_REVISION", "CANCELLED"}, - "IN_PROGRESS": {"COMPLETED", "PENDING_REVISION", "CANCELLED"}, - "COMPLETED": set(), - "CANCELLED": set(), + "PENDING_ANALYSIS": {"PENDING_REVIEW", "PENDING_REVISION", "OPEN", "CLOSED"}, + "PENDING_REVIEW": {"PENDING_REVISION", "OPEN", "CLOSED"}, + "PENDING_REVISION": {"OPEN", "CLOSED"}, + "OPEN": {"IN_PROGRESS", "CLOSED"}, + "IN_PROGRESS": {"COMPLETED", "PENDING_REVISION", "CLOSED"}, + "COMPLETED": {"CLOSED"}, + "CLOSED": set(), } +VALID_PRIORITIES = {"LOW", "MEDIUM", "HIGH"} +PRIORITY_ALIASES = {"URGENT": "HIGH"} -def _requirement_stmt(): - return select(Requirement).options(*REQUIREMENT_LOAD_OPTIONS) - - -def _comment_stmt(): - return select(RequirementComment).options(*COMMENT_LOAD_OPTIONS) - - -def _event_stmt(): - return select(RequirementEvent).options(*EVENT_LOAD_OPTIONS) +COMMENT_LOAD_OPTIONS = (selectinload(RequirementComment.author).selectinload(User.roles),) def list_requirements( @@ -67,28 +67,36 @@ def list_requirements( assignee_user_id: str | None, project_name: str | None, ) -> RequirementListResponse: - stmt = _requirement_stmt() + stmt = select(Requirement) if keyword: like = f"%{keyword.strip()}%" - stmt = stmt.where(or_(Requirement.title.ilike(like), Requirement.code.ilike(like))) + stmt = stmt.where( + or_( + Requirement.title.ilike(like), + Requirement.id.ilike(like), + Requirement.project_name.ilike(like), + ) + ) if status: - stmt = stmt.where(Requirement.status == status) + stmt = stmt.where(Requirement.status == _normalize_status(status)) if priority: - stmt = stmt.where(Requirement.priority == priority) + stmt = stmt.where(Requirement.priority == _normalize_priority(priority)) if assignee_user_id: - stmt = stmt.where(Requirement.assignee_user_id == assignee_user_id) + # 兼容筛选参数:老表中无 assignee 字段,使用 create_user 近似过滤。 + stmt = stmt.where(Requirement.create_user == assignee_user_id) if project_name: stmt = stmt.where(Requirement.project_name == project_name) - requirements = db.execute(stmt.order_by(Requirement.updated_at.desc())).scalars().all() + requirements = db.execute(stmt.order_by(Requirement.update_date.desc())).scalars().all() + user_map = _load_users_for_requirements(db, requirements) return RequirementListResponse( - items=[serialize_requirement(item) for item in requirements], + items=[serialize_requirement(item, user_map=user_map) for item in requirements], total=len(requirements), ) def get_requirement_by_id(db: Session, requirement_id: str) -> Requirement | None: - return db.execute(_requirement_stmt().where(Requirement.id == requirement_id)).scalar_one_or_none() + return db.execute(select(Requirement).where(Requirement.id == requirement_id)).scalar_one_or_none() def create_requirement( @@ -97,24 +105,18 @@ def create_requirement( *, actor: User, ) -> RequirementSummary: - assignee = _load_user_if_exists(db, payload.assignee_user_id) - if payload.assignee_user_id and not assignee: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Assignee not found") - requirement = Requirement( - code=_next_requirement_code(db), title=payload.title.strip(), - description=payload.description.strip(), - status=payload.status, - priority=payload.priority, + descr=payload.description.strip(), + status=_normalize_status(payload.status), + priority=_normalize_priority(payload.priority), project_name=_normalize_str(payload.project_name), - module_name=_normalize_str(payload.module_name), - source=_normalize_str(payload.source), - creator_user_id=actor.id, - assignee_user_id=assignee.id if assignee else None, - due_at=payload.due_at, - closed_at=utcnow() if payload.status == "COMPLETED" else None, + git_url=_normalize_str(payload.source), + branch=_normalize_str(payload.module_name) or "main", + create_user=actor.id, + update_user=actor.id, ) + _apply_progress_by_status(requirement, None) db.add(requirement) db.flush() @@ -122,10 +124,12 @@ def create_requirement( db, requirement_id=requirement.id, actor_user_id=actor.id, - event_type="created", + event_type="CREATE", from_status=None, to_status=requirement.status, - payload={"code": requirement.code}, + before_descr=None, + after_descr=requirement.descr, + remark="创建需求", ) db.commit() @@ -133,7 +137,7 @@ def create_requirement( if not saved: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Requirement save failed") _publish_requirement_change("requirements.changed", saved, action="created") - return serialize_requirement(saved) + return serialize_requirement(saved, user_map={actor.id: actor}) def update_requirement( @@ -147,26 +151,33 @@ def update_requirement( if not requirement: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Requirement not found") + before_descr = requirement.descr update_data = payload.model_dump(exclude_unset=True) - if "assignee_user_id" in update_data: - assignee = _load_user_if_exists(db, update_data["assignee_user_id"]) - if update_data["assignee_user_id"] and not assignee: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Assignee not found") - requirement.assignee_user_id = assignee.id if assignee else None - for field in ["title", "description", "priority", "project_name", "module_name", "source", "due_at"]: - if field in update_data: - value = update_data[field] - setattr(requirement, field, _normalize_str(value) if isinstance(value, str) else value) + if "title" in update_data and update_data["title"] is not None: + requirement.title = update_data["title"].strip() + if "description" in update_data and update_data["description"] is not None: + requirement.descr = update_data["description"].strip() + if "priority" in update_data and update_data["priority"] is not None: + requirement.priority = _normalize_priority(update_data["priority"]) + if "project_name" in update_data: + requirement.project_name = _normalize_str(update_data["project_name"]) + if "module_name" in update_data: + requirement.branch = _normalize_str(update_data["module_name"]) or "main" + if "source" in update_data: + requirement.git_url = _normalize_str(update_data["source"]) + requirement.update_user = actor.id _append_event( db, requirement_id=requirement.id, actor_user_id=actor.id, - event_type="updated", + event_type="EDIT", from_status=requirement.status, to_status=requirement.status, - payload={"fields": sorted(update_data.keys())}, + before_descr=before_descr, + after_descr=requirement.descr, + remark="更新需求", ) db.commit() @@ -174,7 +185,7 @@ def update_requirement( if not saved: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Requirement load failed") _publish_requirement_change("requirements.changed", saved, action="updated") - return serialize_requirement(saved) + return serialize_requirement(saved, user_map={actor.id: actor}) def assign_requirement( @@ -188,23 +199,23 @@ def assign_requirement( if not requirement: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Requirement not found") - assignee = _load_user_if_exists(db, payload.assignee_user_id) - if payload.assignee_user_id and not assignee: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Assignee not found") + assignee_user_id = _normalize_str(payload.assignee_user_id) + if assignee_user_id: + assignee = _load_user_if_exists(db, assignee_user_id) + if not assignee: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Assignee not found") - previous_assignee = requirement.assignee_user_id - requirement.assignee_user_id = assignee.id if assignee else None + requirement.update_user = actor.id _append_event( db, requirement_id=requirement.id, actor_user_id=actor.id, - event_type="assigned", + event_type="EDIT", from_status=requirement.status, to_status=requirement.status, - payload={ - "previous_assignee_user_id": previous_assignee, - "assignee_user_id": requirement.assignee_user_id, - }, + before_descr=requirement.descr, + after_descr=requirement.descr, + remark=f"指派: {assignee_user_id or 'UNASSIGNED'}", ) db.commit() @@ -212,7 +223,10 @@ def assign_requirement( if not saved: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Requirement load failed") _publish_requirement_change("requirements.changed", saved, action="assigned") - return serialize_requirement(saved) + user_map = _load_users_for_requirements(db, [saved]) + if actor.id not in user_map: + user_map[actor.id] = actor + return serialize_requirement(saved, user_map=user_map) def claim_requirement(db: Session, requirement_id: str, *, actor: User) -> RequirementSummary: @@ -235,8 +249,8 @@ def transition_requirement( if not requirement: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Requirement not found") - current_status = requirement.status - target_status = payload.status + current_status = _normalize_status(requirement.status) + target_status = _normalize_status(payload.status) if current_status == target_status: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Status is unchanged") if target_status not in ALLOWED_TRANSITIONS.get(current_status, set()): @@ -246,15 +260,20 @@ def transition_requirement( ) requirement.status = target_status - requirement.closed_at = utcnow() if target_status == "COMPLETED" else None + requirement.update_user = actor.id + if payload.note is not None: + requirement.result_msg = _normalize_str(payload.note) + _apply_progress_by_status(requirement, None) _append_event( db, requirement_id=requirement.id, actor_user_id=actor.id, - event_type="transitioned", + event_type="STATUS_CHANGE", from_status=current_status, to_status=target_status, - payload={"note": _normalize_str(payload.note)}, + before_descr=requirement.descr, + after_descr=requirement.descr, + remark=_normalize_str(payload.note), ) db.commit() @@ -262,7 +281,7 @@ def transition_requirement( if not saved: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Requirement load failed") _publish_requirement_change("requirements.transitioned", saved, action="transitioned") - return serialize_requirement(saved) + return serialize_requirement(saved, user_map={actor.id: actor}) def delete_requirement(db: Session, requirement_id: str, *, actor: User) -> bool: @@ -271,7 +290,6 @@ def delete_requirement(db: Session, requirement_id: str, *, actor: User) -> bool return False deleted_id = requirement.id - deleted_code = requirement.code db.delete(requirement) db.commit() @@ -282,7 +300,7 @@ def delete_requirement(db: Session, requirement_id: str, *, actor: User) -> bool payload={ "action": "deleted", "requirement_id": deleted_id, - "code": deleted_code, + "code": deleted_id, "actor_user_id": actor.id, }, requires_refetch=[ @@ -298,7 +316,8 @@ def delete_requirement(db: Session, requirement_id: str, *, actor: User) -> bool def list_requirement_comments(db: Session, requirement_id: str) -> list[RequirementCommentPublic]: _require_requirement_exists(db, requirement_id) comments = db.execute( - _comment_stmt() + select(RequirementComment) + .options(*COMMENT_LOAD_OPTIONS) .where(RequirementComment.requirement_id == requirement_id) .order_by(RequirementComment.created_at.desc()) ).scalars().all() @@ -324,14 +343,20 @@ def add_requirement_comment( db, requirement_id=requirement.id, actor_user_id=actor.id, - event_type="commented", + event_type="EDIT", from_status=requirement.status, to_status=requirement.status, - payload={"kind": payload.kind}, + before_descr=requirement.descr, + after_descr=requirement.descr, + remark=f"comment:{payload.kind}", ) db.commit() - saved = db.execute(_comment_stmt().where(RequirementComment.id == comment.id)).scalar_one() + saved = db.execute( + select(RequirementComment) + .options(*COMMENT_LOAD_OPTIONS) + .where(RequirementComment.id == comment.id) + ).scalar_one() latest_requirement = get_requirement_by_id(db, requirement.id) if latest_requirement: _publish_requirement_change( @@ -346,34 +371,49 @@ def add_requirement_comment( def list_requirement_events(db: Session, requirement_id: str) -> list[RequirementEventPublic]: _require_requirement_exists(db, requirement_id) events = db.execute( - _event_stmt() + select(RequirementEvent) .where(RequirementEvent.requirement_id == requirement_id) - .order_by(RequirementEvent.created_at.desc(), RequirementEvent.id.desc()) + .order_by(RequirementEvent.create_date.desc(), RequirementEvent.id.desc()) ).scalars().all() - return [serialize_event(item) for item in events] + user_ids = [event.create_user for event in events if event.create_user] + user_map = _load_users_by_ids(db, user_ids) + return [serialize_event(item, user_map=user_map) for item in events] -def serialize_requirement(requirement: Requirement) -> RequirementSummary: +def serialize_requirement( + requirement: Requirement, + *, + user_map: dict[str, User] | None = None, +) -> RequirementSummary: + creator = None + if requirement.create_user: + if user_map and requirement.create_user in user_map: + creator = user_map[requirement.create_user] + return RequirementSummary( id=requirement.id, - code=requirement.code, + code=requirement.id, title=requirement.title, - description=requirement.description, - status=requirement.status, - priority=requirement.priority, + description=requirement.descr or "", + status=_to_api_status(requirement.status), + priority=_to_api_priority(requirement.priority), project_name=requirement.project_name, - module_name=requirement.module_name, - source=requirement.source, - creator_user_id=requirement.creator_user_id, - assignee_user_id=requirement.assignee_user_id, - reviewer_user_id=requirement.reviewer_user_id, - due_at=requirement.due_at, - closed_at=requirement.closed_at, - created_at=requirement.created_at, - updated_at=requirement.updated_at, - creator=serialize_user(requirement.creator) if requirement.creator else None, - assignee=serialize_user(requirement.assignee) if requirement.assignee else None, - reviewer=serialize_user(requirement.reviewer) if requirement.reviewer else None, + module_name=requirement.branch, + source=requirement.git_url, + creator_user_id=requirement.create_user, + assignee_user_id=None, + reviewer_user_id=None, + due_at=None, + closed_at=requirement.update_date if requirement.status in {"COMPLETED", "CLOSED"} else None, + created_at=requirement.create_date, + updated_at=requirement.update_date, + result_msg=requirement.result_msg, + progress_percent=_normalize_progress(requirement.progress_percent), + git_url=requirement.git_url, + branch=requirement.branch, + creator=serialize_user(creator) if creator else None, + assignee=None, + reviewer=None, ) @@ -389,17 +429,28 @@ def serialize_comment(comment: RequirementComment) -> RequirementCommentPublic: ) -def serialize_event(event: RequirementEvent) -> RequirementEventPublic: +def serialize_event( + event: RequirementEvent, + *, + user_map: dict[str, User] | None = None, +) -> RequirementEventPublic: + actor = None + if event.create_user and user_map and event.create_user in user_map: + actor = user_map[event.create_user] return RequirementEventPublic( id=event.id, requirement_id=event.requirement_id, - actor_user_id=event.actor_user_id, + actor_user_id=event.create_user, event_type=event.event_type, from_status=event.from_status, to_status=event.to_status, - payload_json=event.payload_json, - created_at=event.created_at, - actor=serialize_user(event.actor) if event.actor else None, + payload_json={ + "before_descr": event.before_descr, + "after_descr": event.after_descr, + "remark": event.remark, + }, + created_at=event.create_date, + actor=serialize_user(actor) if actor else None, ) @@ -411,16 +462,21 @@ def _append_event( event_type: str, from_status: str | None, to_status: str | None, - payload: dict | None, + before_descr: str | None, + after_descr: str | None, + remark: str | None, ) -> None: db.add( RequirementEvent( requirement_id=requirement_id, - actor_user_id=actor_user_id, event_type=event_type, from_status=from_status, to_status=to_status, - payload_json=payload, + before_descr=before_descr, + after_descr=after_descr, + remark=remark, + create_user=actor_user_id, + update_user=actor_user_id, ) ) @@ -435,9 +491,9 @@ def _publish_requirement_change( payload = { "action": action, "requirement_id": requirement.id, - "code": requirement.code, + "code": requirement.id, "status": requirement.status, - "assignee_user_id": requirement.assignee_user_id, + "assignee_user_id": None, } if extra_payload: payload.update(extra_payload) @@ -460,7 +516,30 @@ def _publish_requirement_change( def _load_user_if_exists(db: Session, user_id: str | None) -> User | None: if not user_id: return None - return db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() + stmt = ( + select(User) + .options(selectinload(User.roles).selectinload(Role.permissions)) + .where(User.id == user_id) + ) + return db.execute(stmt).unique().scalar_one_or_none() + + +def _load_users_by_ids(db: Session, user_ids: list[str]) -> dict[str, User]: + normalized = sorted({user_id for user_id in user_ids if user_id}) + if not normalized: + return {} + stmt = ( + select(User) + .options(selectinload(User.roles).selectinload(Role.permissions)) + .where(User.id.in_(normalized)) + ) + users = db.execute(stmt).unique().scalars().all() + return {user.id: user for user in users} + + +def _load_users_for_requirements(db: Session, requirements: list[Requirement]) -> dict[str, User]: + user_ids = [item.create_user for item in requirements if item.create_user] + return _load_users_by_ids(db, user_ids) def _require_requirement_exists(db: Session, requirement_id: str) -> Requirement: @@ -477,18 +556,61 @@ def _normalize_str(value: str | None) -> str | None: return normalized or None -def _next_requirement_code(db: Session) -> str: - year = datetime.utcnow().year - prefix = f"REQ-{year}-" - existing_codes = db.execute(select(Requirement.code).where(Requirement.code.like(f"{prefix}%"))).scalars().all() - max_seq = 0 - for code in existing_codes: - try: - seq = int(code.removeprefix(prefix)) - except ValueError: - continue - max_seq = max(max_seq, seq) - return f"{prefix}{max_seq + 1:04d}" +def _normalize_status(value: str) -> str: + normalized = (value or "").strip().upper() + if normalized in STATUS_ALIASES: + normalized = STATUS_ALIASES[normalized] + if normalized not in VALID_STATUSES: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid status: {value}") + return normalized + + +def _to_api_status(value: str | None) -> str: + if not value: + return "PENDING_ANALYSIS" + normalized = value.strip().upper() + if normalized in STATUS_ALIASES: + normalized = STATUS_ALIASES[normalized] + if normalized in VALID_STATUSES: + return normalized + return "PENDING_ANALYSIS" + + +def _normalize_priority(value: str) -> str: + normalized = (value or "").strip().upper() + if normalized in PRIORITY_ALIASES: + normalized = PRIORITY_ALIASES[normalized] + if normalized not in VALID_PRIORITIES: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid priority: {value}") + return normalized + + +def _to_api_priority(value: str | None) -> str: + normalized = (value or "").strip().upper() + if normalized == "LOW": + return "low" + if normalized == "HIGH": + return "high" + return "medium" + + +def _normalize_progress(progress_percent: int | None) -> int: + if progress_percent is None: + return 0 + return max(0, min(100, int(progress_percent))) + + +def _apply_progress_by_status(requirement: Requirement, progress_percent: int | None) -> None: + if progress_percent is not None: + requirement.progress_percent = _normalize_progress(progress_percent) + return + if requirement.status == "COMPLETED": + requirement.progress_percent = 100 + return + if requirement.status in ZERO_PROGRESS_STATUSES: + requirement.progress_percent = 0 + return + requirement.progress_percent = _normalize_progress(requirement.progress_percent) def _fire_and_forget(coro: object) -> None: @@ -497,3 +619,300 @@ def _fire_and_forget(coro: object) -> None: except RuntimeError: return loop.create_task(coro) + + +def get_pending_requirement_legacy(db: Session) -> dict | None: + requirement = db.execute( + select(Requirement) + .where(Requirement.status == "OPEN") + .order_by(Requirement.create_date.asc()) + .limit(1) + ).scalar_one_or_none() + if not requirement: + return None + return serialize_requirement_legacy(requirement) + + +def search_requirements_legacy( + db: Session, + *, + page_num: int, + page_size: int, + project_name: str | None, + status_value: str | None, + priority_value: str | None, + title: str | None, +) -> dict: + normalized_page_num = max(page_num, 1) + normalized_page_size = max(page_size, 1) + stmt = select(Requirement) + total_stmt = select(func.count()).select_from(Requirement) + + if project_name: + stmt = stmt.where(Requirement.project_name.ilike(f"%{project_name.strip()}%")) + total_stmt = total_stmt.where(Requirement.project_name.ilike(f"%{project_name.strip()}%")) + if title: + stmt = stmt.where(Requirement.title.ilike(f"%{title.strip()}%")) + total_stmt = total_stmt.where(Requirement.title.ilike(f"%{title.strip()}%")) + if status_value: + db_status = _normalize_status(status_value) + stmt = stmt.where(Requirement.status == db_status) + total_stmt = total_stmt.where(Requirement.status == db_status) + if priority_value: + db_priority = _normalize_priority(priority_value) + stmt = stmt.where(Requirement.priority == db_priority) + total_stmt = total_stmt.where(Requirement.priority == db_priority) + + total = int(db.scalar(total_stmt) or 0) + rows = db.execute( + stmt.order_by(Requirement.create_date.desc()) + .offset((normalized_page_num - 1) * normalized_page_size) + .limit(normalized_page_size) + ).scalars().all() + total_pages = math.ceil(total / normalized_page_size) if total > 0 else 0 + return { + "content": [serialize_requirement_legacy(item) for item in rows], + "totalElements": total, + "totalPages": total_pages, + "size": normalized_page_size, + "number": normalized_page_num - 1, + } + + +def get_requirement_legacy(db: Session, requirement_id: str) -> dict: + requirement = _require_requirement_exists(db, requirement_id) + return serialize_requirement_legacy(requirement) + + +def update_status_legacy( + db: Session, + *, + requirement_id: str, + status_value: str, + result_msg: str | None, + progress_percent: int | None, + actor_user_id: str | None, +) -> None: + requirement = _require_requirement_exists(db, requirement_id) + from_status = requirement.status + before_descr = requirement.descr + + requirement.status = _normalize_status(status_value) + if _normalize_str(result_msg): + requirement.result_msg = _normalize_str(result_msg) + _apply_progress_by_status(requirement, progress_percent) + if actor_user_id: + requirement.update_user = actor_user_id + + _append_event( + db, + requirement_id=requirement.id, + actor_user_id=actor_user_id, + event_type="STATUS_CHANGE", + from_status=from_status, + to_status=requirement.status, + before_descr=before_descr, + after_descr=requirement.descr, + remark=_normalize_str(result_msg), + ) + db.commit() + + +def analyze_requirement_legacy( + db: Session, + *, + requirement_id: str, + descr: str | None, + progress_percent: int | None, + actor_user_id: str | None, +) -> dict: + requirement = _require_requirement_exists(db, requirement_id) + if requirement.status != "PENDING_ANALYSIS": + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only PENDING_ANALYSIS can be analyzed") + + from_status = requirement.status + before_descr = requirement.descr + if descr is not None: + requirement.descr = descr + requirement.status = "PENDING_REVIEW" + requirement.progress_percent = _normalize_progress(progress_percent) + if actor_user_id: + requirement.update_user = actor_user_id + + _append_event( + db, + requirement_id=requirement.id, + actor_user_id=actor_user_id, + event_type="ANALYZE", + from_status=from_status, + to_status=requirement.status, + before_descr=before_descr, + after_descr=requirement.descr, + remark=None, + ) + db.commit() + return serialize_requirement_legacy(requirement) + + +def design_requirement_legacy( + db: Session, + *, + requirement_id: str, + descr: str | None, + actor_user_id: str | None, +) -> dict: + requirement = _require_requirement_exists(db, requirement_id) + if requirement.status != "PENDING_ANALYSIS": + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only PENDING_ANALYSIS can be designed") + + from_status = requirement.status + before_descr = requirement.descr + if descr is not None: + requirement.descr = descr + requirement.status = "PENDING_ANALYSIS" + requirement.progress_percent = 0 + if actor_user_id: + requirement.update_user = actor_user_id + + _append_event( + db, + requirement_id=requirement.id, + actor_user_id=actor_user_id, + event_type="DESIGN", + from_status=from_status, + to_status=requirement.status, + before_descr=before_descr, + after_descr=requirement.descr, + remark=None, + ) + db.commit() + return serialize_requirement_legacy(requirement) + + +def review_requirement_legacy( + db: Session, + *, + requirement_id: str, + decision: str, + descr: str | None, + comment: str | None, + actor_user_id: str | None, +) -> dict: + requirement = _require_requirement_exists(db, requirement_id) + normalized_decision = (decision or "").strip().upper() + if normalized_decision not in {"TO_REVISION", "TO_OPEN"}: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid review decision") + + from_status = requirement.status + before_descr = requirement.descr + if descr is not None: + requirement.descr = descr + requirement.status = "PENDING_REVISION" if normalized_decision == "TO_REVISION" else "OPEN" + requirement.progress_percent = 0 + if actor_user_id: + requirement.update_user = actor_user_id + + _append_event( + db, + requirement_id=requirement.id, + actor_user_id=actor_user_id, + event_type="REVIEW", + from_status=from_status, + to_status=requirement.status, + before_descr=before_descr, + after_descr=requirement.descr, + remark=_normalize_str(comment), + ) + db.commit() + return serialize_requirement_legacy(requirement) + + +def list_lifecycle_legacy(db: Session, requirement_id: str) -> list[dict]: + _require_requirement_exists(db, requirement_id) + logs = db.execute( + select(RequirementEvent) + .where(RequirementEvent.requirement_id == requirement_id) + .order_by(RequirementEvent.create_date.asc(), RequirementEvent.id.asc()) + ).scalars().all() + return [serialize_lifecycle_log_legacy(item) for item in logs] + + +def get_history_options_legacy(db: Session) -> dict: + requirements = db.execute( + select(Requirement) + .order_by(Requirement.create_date.desc()) + .limit(200) + ).scalars().all() + project_names = [] + git_urls = [] + branches = [] + seen_project = set() + seen_git = set() + seen_branch = set() + for item in requirements: + if item.project_name: + value = item.project_name.strip() + if value and value not in seen_project: + seen_project.add(value) + project_names.append(value) + if item.git_url: + value = item.git_url.strip() + if value and value not in seen_git: + seen_git.add(value) + git_urls.append(value) + if item.branch: + value = item.branch.strip() + if value and value not in seen_branch: + seen_branch.add(value) + branches.append(value) + return { + "projectNames": project_names, + "gitUrls": git_urls, + "branches": branches, + } + + +def serialize_requirement_legacy(requirement: Requirement) -> dict: + try: + status_value = _normalize_status(requirement.status or "PENDING_ANALYSIS") + except HTTPException: + status_value = "PENDING_ANALYSIS" + + try: + priority_value = _normalize_priority(requirement.priority or "MEDIUM") + except HTTPException: + priority_value = "MEDIUM" + + return { + "id": requirement.id, + "title": requirement.title, + "projectName": requirement.project_name, + "gitUrl": requirement.git_url, + "branch": requirement.branch, + "descr": requirement.descr, + "resultMsg": requirement.result_msg, + "progressPercent": _normalize_progress(requirement.progress_percent), + "status": status_value, + "priority": priority_value, + "createDate": requirement.create_date.isoformat() if requirement.create_date else None, + "createUser": requirement.create_user, + "updateDate": requirement.update_date.isoformat() if requirement.update_date else None, + "updateUser": requirement.update_user, + } + + +def serialize_lifecycle_log_legacy(log: RequirementEvent) -> dict: + return { + "id": log.id, + "requirementId": log.requirement_id, + "eventType": log.event_type, + "fromStatus": log.from_status, + "toStatus": log.to_status, + "beforeDescr": log.before_descr, + "afterDescr": log.after_descr, + "remark": log.remark, + "createDate": log.create_date.isoformat() if log.create_date else None, + "createUser": log.create_user, + "updateDate": log.update_date.isoformat() if log.update_date else None, + "updateUser": log.update_user, + } diff --git a/api/app/services/seed_service.py b/api/app/services/seed_service.py index 5e1040a..0d915f2 100644 --- a/api/app/services/seed_service.py +++ b/api/app/services/seed_service.py @@ -366,7 +366,7 @@ DEFAULT_MENUS: list[dict[str, object]] = [ }, { "code": "admin.mindmap", - "name": "题库统计", + "name": "思维导图", "path": "/admin/mindmap", "icon": "ChartBar", "parent_code": None, @@ -509,7 +509,7 @@ DEFAULT_MENUS: list[dict[str, object]] = [ }, { "code": "admin.diary", - "name": "上帝视角", + "name": "日记管理", "path": "/admin/diary", "icon": "Eye", "parent_code": None, diff --git a/api/app/services/todo_service.py b/api/app/services/todo_service.py index 843e0b6..bfbbacb 100644 --- a/api/app/services/todo_service.py +++ b/api/app/services/todo_service.py @@ -1,65 +1,75 @@ from __future__ import annotations import asyncio - +from sqlalchemy import func, select +from sqlalchemy.orm import Session from fastapi import HTTPException, status -from sqlalchemy import or_, select -from sqlalchemy.orm import Session, selectinload from ..models.base import utcnow +from ..models.mind_map import MindMap from ..models.todo import Todo from ..models.user import User from ..schemas.todo import ( TodoCreateRequest, TodoListResponse, + TodoMindMapInitResponse, TodoSummary, TodoTransitionRequest, TodoUpdateRequest, ) +from .mind_map_service import build_initial_mind_map_data from .push_service import publish_topic -from .user_service import serialize_user -TODO_LOAD_OPTIONS = ( - selectinload(Todo.creator).selectinload(User.roles), - selectinload(Todo.assignee).selectinload(User.roles), -) TOPIC_NAME = "todos" ALLOWED_TRANSITIONS: dict[str, set[str]] = { - "TODO": {"IN_PROGRESS", "DONE"}, - "IN_PROGRESS": {"TODO", "DONE"}, - "DONE": {"TODO", "IN_PROGRESS"}, + "SCHEDULED": {"IN_PROGRESS", "COMPLETED", "CANCELLED", "EXPIRED"}, + "IN_PROGRESS": {"SCHEDULED", "COMPLETED", "CANCELLED", "EXPIRED"}, + "COMPLETED": {"SCHEDULED", "IN_PROGRESS", "CANCELLED"}, + "CANCELLED": {"SCHEDULED", "IN_PROGRESS"}, + "EXPIRED": {"SCHEDULED", "IN_PROGRESS", "CANCELLED", "COMPLETED"}, } -def _todo_stmt(): - return select(Todo).options(*TODO_LOAD_OPTIONS) - - def list_todos( db: Session, *, - keyword: str | None, + title: str | None, status_filter: str | None, priority: str | None, - assignee_user_id: str | None, + page_num: int, + page_size: int, + actor: User, ) -> TodoListResponse: - stmt = _todo_stmt() - if keyword: - like = f"%{keyword.strip()}%" - stmt = stmt.where(or_(Todo.title.ilike(like), Todo.description.ilike(like))) + filters = [Todo.create_user == actor.username] + + if title: + filters.append(Todo.title.ilike(f"%{title.strip()}%")) if status_filter: - stmt = stmt.where(Todo.status == status_filter) + filters.append(Todo.status == status_filter) if priority: - stmt = stmt.where(Todo.priority == priority) - if assignee_user_id: - stmt = stmt.where(Todo.assignee_user_id == assignee_user_id) + filters.append(Todo.priority == priority) - todos = db.execute(stmt.order_by(Todo.updated_at.desc())).scalars().all() - return TodoListResponse(items=[serialize_todo(item) for item in todos], total=len(todos)) + total_stmt = select(func.count()).select_from(Todo).where(*filters) + total = db.execute(total_stmt).scalar_one() + + stmt = ( + select(Todo) + .where(*filters) + .order_by(Todo.create_date.desc()) + .offset(page_num * page_size) + .limit(page_size) + ) + items = db.execute(stmt).scalars().all() + return TodoListResponse(items=[serialize_todo(item) for item in items], total=total) -def get_todo_by_id(db: Session, todo_id: str) -> Todo | None: - return db.execute(_todo_stmt().where(Todo.id == todo_id)).scalar_one_or_none() +def get_todo_by_id(db: Session, todo_id: str, *, actor: User | None = None) -> Todo | None: + todo = db.execute(select(Todo).where(Todo.id == todo_id)).scalar_one_or_none() + if not todo: + return None + if actor and todo.create_user != actor.username: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to access todo") + return todo def create_todo( @@ -67,20 +77,22 @@ def create_todo( payload: TodoCreateRequest, *, actor: User, + syncing: bool = False, ) -> TodoSummary: - assignee = _load_user_if_exists(db, payload.assignee_user_id) - if payload.assignee_user_id and not assignee: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Assignee not found") - + now = utcnow() todo = Todo( title=payload.title.strip(), - description=payload.description.strip(), + descr=_normalize_str(payload.descr) or "", status=payload.status, priority=payload.priority, - assignee_user_id=assignee.id if assignee else None, - creator_user_id=actor.id, - due_at=payload.due_at, - completed_at=utcnow() if payload.status == "DONE" else None, + start_time=payload.start_time, + due_date=payload.due_date, + expire_time=payload.expire_time, + calendar_event_id=_normalize_str(payload.calendar_event_id), + create_user=actor.username, + update_user=actor.username, + create_date=now, + update_date=now, ) db.add(todo) db.commit() @@ -88,6 +100,13 @@ def create_todo( saved = get_todo_by_id(db, todo.id) if not saved: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Todo save failed") + + if not (payload.is_sync or syncing): + from .calendar_event_service import sync_from_todo_create + + sync_from_todo_create(db, todo=saved, actor=actor) + saved = get_todo_by_id(db, todo.id) or saved + _publish_todo_change("todos.created", saved, action="created") return serialize_todo(saved) @@ -98,28 +117,32 @@ def update_todo( payload: TodoUpdateRequest, *, actor: User, + syncing: bool = False, ) -> TodoSummary: - todo = get_todo_by_id(db, todo_id) + todo = get_todo_by_id(db, todo_id, actor=actor) if not todo: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") update_data = payload.model_dump(exclude_unset=True) - if "assignee_user_id" in update_data: - assignee = _load_user_if_exists(db, update_data["assignee_user_id"]) - if update_data["assignee_user_id"] and not assignee: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Assignee not found") - todo.assignee_user_id = assignee.id if assignee else None - - for field in ["title", "description", "priority", "due_at"]: + for field in ["title", "descr", "status", "priority", "start_time", "due_date", "expire_time", "calendar_event_id"]: if field in update_data: value = update_data[field] setattr(todo, field, _normalize_str(value) if isinstance(value, str) else value) + todo.update_user = actor.username + todo.update_date = utcnow() db.commit() saved = get_todo_by_id(db, todo.id) if not saved: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Todo load failed") + + if not (payload.is_sync or syncing): + from .calendar_event_service import sync_from_todo_update + + sync_from_todo_update(db, todo=saved, actor=actor) + saved = get_todo_by_id(db, todo.id) or saved + _publish_todo_change("todos.updated", saved, action="updated") return serialize_todo(saved) @@ -130,8 +153,9 @@ def transition_todo( payload: TodoTransitionRequest, *, actor: User, + syncing: bool = False, ) -> TodoSummary: - todo = get_todo_by_id(db, todo_id) + todo = get_todo_by_id(db, todo_id, actor=actor) if not todo: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") @@ -146,18 +170,94 @@ def transition_todo( ) todo.status = target_status - todo.completed_at = utcnow() if target_status == "DONE" else None + todo.update_user = actor.username + todo.update_date = utcnow() db.commit() saved = get_todo_by_id(db, todo.id) if not saved: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Todo load failed") + + if not (payload.is_sync or syncing): + from .calendar_event_service import sync_from_todo_transition + + sync_from_todo_transition(db, todo=saved, actor=actor) + saved = get_todo_by_id(db, todo.id) or saved + _publish_todo_change("todos.transitioned", saved, action="transitioned") return serialize_todo(saved) -def delete_todo(db: Session, todo_id: str, *, actor: User) -> dict[str, bool]: - todo = get_todo_by_id(db, todo_id) +def complete_todo( + db: Session, + todo_id: str, + *, + actor: User, + syncing: bool = False, +) -> TodoSummary: + todo = get_todo_by_id(db, todo_id, actor=actor) + if not todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") + + todo.status = "COMPLETED" + todo.update_user = actor.username + todo.update_date = utcnow() + db.commit() + + saved = get_todo_by_id(db, todo.id) + if not saved: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Todo load failed") + + if not syncing: + from .calendar_event_service import sync_from_todo_transition + + sync_from_todo_transition(db, todo=saved, actor=actor) + saved = get_todo_by_id(db, todo.id) or saved + + _publish_todo_change("todos.completed", saved, action="completed") + return serialize_todo(saved) + + +def init_todo_mindmap( + db: Session, + todo_id: str, + *, + actor: User, +) -> TodoMindMapInitResponse: + todo = get_todo_by_id(db, todo_id, actor=actor) + if not todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") + + mind_map = db.execute(select(MindMap).where(MindMap.id == todo_id)).scalar_one_or_none() + if mind_map: + if mind_map.create_user != actor.username: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No permission to access mind map") + else: + now = utcnow() + mind_map = MindMap( + id=todo_id, + map_name=todo.title, + descr=todo.descr or "", + map_data=build_initial_mind_map_data(todo.title), + create_user=actor.username, + update_user=actor.username, + create_date=now, + update_date=now, + ) + db.add(mind_map) + db.commit() + db.refresh(mind_map) + + return TodoMindMapInitResponse( + id=mind_map.id, + map_name=mind_map.map_name, + descr=mind_map.descr, + map_data=mind_map.map_data or "", + ) + + +def delete_todo(db: Session, todo_id: str, *, actor: User, syncing: bool = False) -> dict[str, bool]: + todo = get_todo_by_id(db, todo_id, actor=actor) if not todo: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") @@ -165,11 +265,16 @@ def delete_todo(db: Session, todo_id: str, *, actor: User) -> dict[str, bool]: db.delete(todo) db.commit() + if not syncing: + from .calendar_event_service import sync_from_todo_delete + + sync_from_todo_delete(db, todo=todo, actor=actor) + _fire_and_forget( publish_topic( TOPIC_NAME, name="todos.deleted", - payload={"action": "deleted", "todo_id": deleted_id, "actor_user_id": actor.id}, + payload={"action": "deleted", "todo_id": deleted_id, "actor_user": actor.username}, requires_refetch=["/api/v1/todos"], dedupe_key=f"todos:deleted:{deleted_id}", ) @@ -181,17 +286,17 @@ def serialize_todo(todo: Todo) -> TodoSummary: return TodoSummary( id=todo.id, title=todo.title, - description=todo.description, + descr=todo.descr, status=todo.status, priority=todo.priority, - assignee_user_id=todo.assignee_user_id, - creator_user_id=todo.creator_user_id, - due_at=todo.due_at, - completed_at=todo.completed_at, - created_at=todo.created_at, - updated_at=todo.updated_at, - creator=serialize_user(todo.creator) if todo.creator else None, - assignee=serialize_user(todo.assignee) if todo.assignee else None, + start_time=todo.start_time, + due_date=todo.due_date, + expire_time=todo.expire_time, + calendar_event_id=todo.calendar_event_id, + create_date=todo.create_date, + create_user=todo.create_user, + update_date=todo.update_date, + update_user=todo.update_user, ) @@ -201,28 +306,18 @@ def _publish_todo_change(event_name: str, todo: Todo, *, action: str) -> None: "todo_id": todo.id, "status": todo.status, "priority": todo.priority, - "assignee_user_id": todo.assignee_user_id, } _fire_and_forget( publish_topic( TOPIC_NAME, name=event_name, payload=payload, - requires_refetch=[ - "/api/v1/todos", - f"/api/v1/todos/{todo.id}", - ], + requires_refetch=["/api/v1/todos"], dedupe_key=f"todos:{action}:{todo.id}", ) ) -def _load_user_if_exists(db: Session, user_id: str | None) -> User | None: - if not user_id: - return None - return db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() - - def _normalize_str(value: str | None) -> str | None: if value is None: return None diff --git a/docker-compose.yml b/docker-compose.yml index 69070c6..770d25f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,7 @@ services: db: + profiles: + - local-db image: ${POSTGRES_IMAGE:-docker.m.daocloud.io/pgvector/pgvector:pg16} container_name: fquiz-db environment: @@ -32,15 +34,18 @@ services: PIP_DEFAULT_TIMEOUT: ${PIP_DEFAULT_TIMEOUT:-300} PIP_RETRIES: ${PIP_RETRIES:-20} container_name: fquiz-api - depends_on: - db: - condition: service_healthy environment: API_HOST: ${API_HOST:-0.0.0.0} API_PORT: ${API_PORT:-8000} API_CORS_ORIGINS: ${API_CORS_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000} API_CORS_ORIGIN_REGEX: ${API_CORS_ORIGIN_REGEX:-} - DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://fquiz:fquiz@db:5432/fquiz} + DATABASE_URL: ${DATABASE_URL:-} + DB_HOST: ${DB_HOST:-223.109.142.84} + DB_PORT: ${DB_PORT:-5432} + DB_NAME: ${DB_NAME:-postgres} + DB_SCHEMA: ${DB_SCHEMA:-public} + DB_USERNAME: ${DB_USERNAME:-postgres} + DB_PASSWORD: ${DB_PASSWORD:-1qazZAQ!} JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-this-in-production} ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-15} REFRESH_TOKEN_EXPIRE_DAYS: ${REFRESH_TOKEN_EXPIRE_DAYS:-30} diff --git a/memory/2026-04-19.md b/memory/2026-04-19.md index e6cc2c7..2edebe8 100644 --- a/memory/2026-04-19.md +++ b/memory/2026-04-19.md @@ -1323,3 +1323,37 @@ - 本次按任务约束未执行构建/编译与额外回归测试。 - 当前仓库存在多需求并行改动;技能脚本 `changedFiles` 为工作区整体视图,不仅限热搜模块。 - 已同步更新长期记忆 `MEMORY.md`:补充 `热搜` 菜单迁移口径(入口复用 + 后端能力底座)。 + +## Work Log - Radix 菜单改造(Phase A + Phase B) + +- 背景: + - 按需求“先落地 Phase A + Phase B”推进后台菜单交互改造,目标是在不改接口契约的前提下统一菜单交互形态。 + - 执行策略:最小改动,仅替换前端交互承载组件,不调整既有业务逻辑与权限判断。 + +- 本次改动: + - Phase A(后台壳层菜单) + - `web/src/app/admin/layout.tsx` + - 引入 `DropdownMenu`。 + - 左侧菜单改为 `md` 及以上显示;移动端新增“菜单”下拉入口,基于 `menuTree` 扁平化列表展示可访问菜单项。 + - 顶部账号区将“返回首页/退出登录”按钮收口为“账号”下拉菜单。 + - Phase B(操作列统一) + - 新增 `web/src/components/row-action-menu.tsx` + - 封装基于 `@radix-ui/themes` 的通用行操作下拉组件 `RowActionMenu`。 + - `web/src/app/admin/users/page.tsx` + - 将“启用/禁用、改密码、删除”并排按钮改为 `RowActionMenu`。 + - `web/src/app/admin/requirements/page.tsx` + - 将“领取、开始处理/标记完成、删除”操作改为 `RowActionMenu`,保留原状态流转与删除确认逻辑。 + - `web/src/app/admin/menus/page.tsx` + - 将“编辑/删除”操作改为 `RowActionMenu`,保留受保护菜单删除拦截逻辑。 + +- 验证: + - `npm run build:web`:通过(Next.js 16,TypeScript 阶段通过,页面静态生成完成)。 + - `npm run lint:web`:失败,存在仓库既有问题(非本次改动引入): + - `web/src/app/admin/hot-search/page.tsx`:`react-hooks/set-state-in-effect`、`react-hooks/exhaustive-deps` + - `web/src/app/admin/jwt-generator/page.tsx`:`react-hooks/preserve-manual-memoization` + - `web/src/app/admin/life-countdown/page.tsx`:`react-hooks/set-state-in-effect` + - `web/src/app/admin/password/page.tsx`:`react-hooks/exhaustive-deps` + +- 风险与影响: + - 行操作从直出按钮改为二次点击菜单,短期用户习惯需要适配。 + - 移动端后台导航改为下拉入口后,首屏更紧凑;但需要依赖菜单数据加载成功才可展开跳转项。 diff --git a/memory/2026-04-22.md b/memory/2026-04-22.md new file mode 100644 index 0000000..fabb4ab --- /dev/null +++ b/memory/2026-04-22.md @@ -0,0 +1,320 @@ +## Work Log - 外部 PostgreSQL 连接切换(2026-04-22) + +- 背景:按“改成连外部 PG 库”要求,将 API 默认数据库连接从 compose 内 `db` 服务切换为外部 PostgreSQL,并保留本地 `db` 容器为可选能力。 + +- 本次改动(最小闭环): + - `api/app/core/config.py` + - 新增 `DB_HOST/DB_PORT/DB_NAME/DB_SCHEMA/DB_USERNAME/DB_PASSWORD` 配置项。 + - `DATABASE_URL` 改为可选覆盖项;未设置时自动由 `DB_*` 组装 PostgreSQL DSN。 + - `api/app/core/database.py` + - 统一使用 `settings.resolved_database_url` 建连。 + - PostgreSQL 场景注入 `options=-csearch_path=`,对齐 `currentSchema` 语义。 + - `docker-compose.yml` + - `api` 移除对 `db` 的强依赖(不再 `depends_on db`)。 + - `api` 注入 `DB_*` 外部库环境变量,`DATABASE_URL` 作为可选覆盖。 + - `db` 服务改为 `profiles: [local-db]`,默认不启动。 + - `.env.example` + - 数据库示例变量切换为外部库口径(含 `DB_*`)。 + - `README.md` + - 更新 compose 启动说明:默认外部库;本地 DB 需显式 `--profile local-db`。 + +- 验证: + - `python3 -m py_compile api/app/core/config.py api/app/core/database.py` -> `py_compile_ok` + - `docker compose config` -> `compose_config_ok`,解析结果含 `DB_HOST/DB_PORT/DB_NAME/DB_SCHEMA/DB_USERNAME` 且 `DATABASE_URL` 允许为空覆盖。 + +- 风险与影响: + - 默认 `docker compose up --build -d` 不再自动启动本地 DB;需要本地库时必须显式启用 `local-db` profile。 + - 若外部库网络不通或凭据错误,API 启动后会因数据库不可达报错;可通过设置 `DATABASE_URL` 快速回退到其他连接串。 + +## Work Log - 登录页动效改造(2026-04-22) + +- 背景:用户要求登录页增强动效,增加“动态小怪兽 + 眼睛跟随鼠标 + 输入密码时挪开视线”的交互。 + +- 本次改动(最小闭环,仅改前台首页): + - `web/src/app/page.tsx` + - 保留既有登录/注册提交链路(`login/register`)、记住密码逻辑与错误处理,不改接口契约。 + - 新增怪兽视线状态:`gaze` + `monsterRef` + `pointermove` 监听,实现眼球跟随鼠标。 + - 新增密码聚焦状态:`passwordFocused`;密码输入框 `onFocus/onBlur` 驱动怪兽“挪开视线”(瞳孔偏移 + 眼睑下落 + 身体倾斜)。 + - 重做未登录页视觉:浮动渐变背景、怪兽悬浮动画、阴影脉冲动画,并保持桌面/移动端自适应布局。 + +- 验证: + - `cd web && npx eslint src/app/page.tsx` -> 通过(0 error)。 + - `npm run lint:web` -> 失败(仓库已有历史问题,集中在 `admin/hot-search`、`admin/jwt-generator`、`admin/life-countdown`,与本次登录页改动无关)。 + +- 风险与影响: + - 本次改动将动画样式内联在 `web/src/app/page.tsx` 的 `style jsx` 中,后续若首页风格继续扩展,建议再抽离到独立样式模块以降低单文件复杂度。 + +## Work Log - 前端组件库从 Radix UI 迁移到 Ant Design(2026-04-22) + +- 背景:按“弃用 Radix UI,改成 Ant Design”要求,前端在不改业务逻辑前提下完成组件层迁移。 + +- 本次改动(最小闭环): + - 依赖迁移: + - `web/package.json` 移除 `@radix-ui/themes`、`@radix-ui/react-dialog`、`@radix-ui/react-select`。 + - 新增 `antd` 依赖;根 `package-lock.json` 同步更新。 + - 兼容层落地: + - 新增 `web/src/components/ui-antd.tsx`,提供 `Button/Card/Flex/Text/Heading/TextField/TextArea/Select/Dialog/DropdownMenu/Callout/Table/Checkbox/Theme` 兼容导出,底层改由 Ant Design 承接。 + - 新增 `web/src/types/antd.d.ts`(当前安装源下 `antd` 包不含 `.d.ts` 时的类型声明兜底)。 + - 批量替换: + - `web/src/app/**` 与 `web/src/components/row-action-menu.tsx` 的 `@radix-ui/themes` 导入统一替换为 `@/components/ui-antd`。 + - `web/src/app/layout.tsx` 移除 `@radix-ui/themes/styles.css`,改为 `antd/dist/reset.css`,`Theme` 改由兼容层提供。 + +- 验证: + - `npm run build:web` -> 通过(Next.js build + TypeScript + 静态页面生成全部成功)。 + +- 风险与影响: + - 兼容层采用“Radix API 形态 + AntD 实现”策略,短期可稳定过渡;后续若需发挥 AntD 原生能力(如 Table 高级功能、Form 生态),建议分阶段把页面改为直接使用 AntD 原生 API,逐步下线兼容层。 + +## Work Log - 前端依赖残留清理(2026-04-22) + +- 背景:用户要求“清理”组件库迁移后的残留依赖。 + +- 本次处理: + - 清理校验范围统一为源码导入 + `web/package.json` + 根/子锁文件。 + - `web/package-lock.json` 已重新对齐 `web/package.json`,移除 `@radix-ui/*` 残留并保留 `antd` 依赖树。 + - 根 `package-lock.json` 未检出 `@radix-ui/*` 残留。 + - 已删除本地 `node_modules` 与 `web/node_modules`,避免此前安装异常产生的脏依赖目录影响后续排查。 + +- 验证: + - `rg -n "@radix-ui/..." web/src web/package.json web/package-lock.json package-lock.json` 无命中。 + +- 说明: + - 当前环境下多次 `npm install` 出现 `TAR_ENTRY_ERROR ENOENT`(Next 解包阶段),属于本地依赖安装层异常,不影响仓库内代码与锁文件的残留清理结论。 + +## Work Log - 需求管理兼容老工程表与逻辑(2026-04-22) + +- 背景:按“用老工程 `quiz` 的需求表结构与流程逻辑改造 `fquiz`”要求,目标是在保留现有 `/api/v1/requirements` 可用的前提下,切换底层到老表并补齐老接口兼容。 + +- 本次改动(最小闭环): + - ORM 表结构改造(对齐老工程): + - `api/app/models/requirement.py` + - `Requirement` 主表由 `requirements` 切换为 `project_requirement`。 + - 字段改为老口径:`title/project_name/git_url/branch/descr/result_msg/progress_percent/status/priority/create_date/create_user/update_date/update_user`。 + - 生命周期事件表切到 `project_requirement_log`(`RequirementEvent`)。 + - 保留 `requirement_comments` 作为现有 API 注释能力的兼容扩展,外键改指向 `project_requirement.id`。 + - 服务层逻辑改造(参考老工程): + - `api/app/services/requirement_service.py` + - 状态集改为老口径:`PENDING_ANALYSIS/PENDING_REVIEW/PENDING_REVISION/OPEN/IN_PROGRESS/COMPLETED/CLOSED`。 + - 兼容映射:`CANCELLED -> CLOSED`。 + - 进度规则对齐老逻辑:`COMPLETED=100`;`PENDING_ANALYSIS/PENDING_REVIEW/PENDING_REVISION/OPEN=0`;其余区间归一化到 `0-100`。 + - `/api/v1/requirements` 仍可用:序列化层将老表字段映射回现有前端字段(`description<-descr`、`source<-git_url`、`module_name<-branch` 等)。 + - 事件列表改读 `project_requirement_log`,并将 `before_descr/after_descr/remark` 投影为 `payload_json`。 + - 新增 legacy 业务函数:`search/get/pending/update_status/analyze/review/lifecycle/history-options`。 + - 旧接口兼容层新增: + - 新增 `api/app/api/project_requirement.py`,提供: + - `POST /api/project/requirement/search` + - `GET /api/project/requirement/get/{id}` + - `GET /api/project/requirement/pending` + - `POST /api/project/requirement/{id}/status` + - `POST /api/project/requirement/{id}/analyze` + - `POST /api/project/requirement/{id}/review` + - `GET /api/project/requirement/{id}/lifecycle` + - `GET /api/project/requirement/history-options` + - `api/app/api/router.py` 改为组合路由:保留 `/api/v1/*`,并额外挂载 `/api/project/requirement/*`。 + - 前端类型与状态枚举兼容: + - `web/src/types/auth.ts`:`RequirementStatus` 新增 `PENDING_REVIEW/CLOSED`;`RequirementEvent.id` 改为 `string`;`RequirementSummary` 补充 `result_msg/progress_percent/git_url/branch`。 + - `web/src/app/admin/requirements/page.tsx` + - `web/src/app/admin/requirements/new/page.tsx` + - `web/src/app/admin/requirements/[id]/page.tsx` + - 状态文案/选项新增 `PENDING_REVIEW`、`CLOSED`;详情页状态流转映射同步老逻辑。 + +- 验证: + - `python3 -m compileall api/app/services/requirement_service.py api/app/api/project_requirement.py api/app/models/requirement.py api/app/schemas/requirement.py api/app/api/router.py` -> 通过。 + - `npm --workspace web exec -- eslint src/app/admin/requirements/page.tsx src/app/admin/requirements/new/page.tsx src/app/admin/requirements/[id]/page.tsx src/types/auth.ts` -> 通过。 + - `npm run -s lint:web` -> 失败(仓库既有问题:`hot-search/jwt-generator/life-countdown` 等页面,非本次改造引入)。 + - 运行态接口自检未执行:当前环境缺少 `fastapi` 运行依赖(`ModuleNotFoundError: fastapi`),因此未做本地启动路由验证。 + +- 风险与影响: + - 该改造面向“老表兼容”,若数据库中同时存在新旧两套需求表,后续数据归一需额外迁移策略。 + - 由于老表无显式 `assignee/reviewer/due_at` 字段,`/api/v1/requirements` 中对应字段目前为兼容占位(多数返回 `null`)。 + - `project_requirement_log` 在历史库中若字段口径有差异(例如时间列名或类型差异),需按实际库结构微调 ORM 映射。 +- 增补兼容接口:`POST /api/project/requirement/{id}/design`(对应老工程 DESIGN 流程,保持 `PENDING_ANALYSIS` 并写入生命周期日志)。 + +## Work Log - 待办管理按 quiz 表与页面重构(2026-04-22) + +- 背景:按“改用 quiz 的表,照抄 quiz 逻辑和页面”要求,对 `fquiz` 待办模块做整链路重构(后端表结构/状态机/API + 前端页面交互)。 + +- 本次改动(最小闭环): + - 后端表结构与字段口径对齐 `quiz`: + - `api/app/models/todo.py` + - 表名由 `todos` 改为 `todo`。 + - 主键改为 32 位 hex 字符串(`uuid4().hex`)。 + - 字段切换为 `title/descr/status/priority/start_time/due_date/expire_time/calendar_event_id/create_date/create_user/update_date/update_user`。 + - 索引对齐:`idx_todo_status/idx_todo_priority/idx_todo_due_date/idx_todo_expire_time`。 + - 后端 schema 与服务逻辑重构: + - `api/app/schemas/todo.py` + - 状态改为 `SCHEDULED/IN_PROGRESS/COMPLETED/CANCELLED/EXPIRED`。 + - 优先级改为 `LOW/MEDIUM/HIGH`。 + - 请求/响应字段改为 quiz 风格(`descr/start_time/due_date/expire_time`)。 + - 新增 `TodoMindMapInitResponse`。 + - `api/app/services/todo_service.py` + - 列表查询改为 quiz 逻辑:按当前登录用户名过滤 `create_user`,支持 `title/status/priority`,并支持分页参数 `page_num/page_size`。 + - 新增 owner 校验(非创建人不可读写删改)。 + - 新增 `complete_todo` 与 `init_todo_mindmap`。 + - 保留/兼容 `transition_todo`,状态流转规则切换到新状态机。 + - 后端路由补齐: + - `api/app/api/v1/todos.py` + - 列表接口支持 `title/status/priority/page_num/page_size`(兼容 `keyword`)。 + - 新增 `POST /api/v1/todos/{todo_id}/complete`。 + - 新增 `POST /api/v1/todos/{todo_id}/init-mindmap`。 + - 前端类型与页面重构: + - `web/src/types/auth.ts` + - `TodoStatus/TodoPriority/TodoSummary` 改为 quiz 字段与状态口径。 + - `web/src/app/admin/todos/page.tsx` + - 页面按 quiz 交互重写:默认筛选 `SCHEDULED`、查询/重置、分页、新增、编辑、详情、完成、删除、分析(init-mindmap)。 + - 状态与优先级中文标签、完成态/过期态禁用“完成”按钮,行为与 quiz 页面一致。 + +- 验证: + - `python3 -m py_compile api/app/models/todo.py api/app/schemas/todo.py api/app/services/todo_service.py api/app/api/v1/todos.py` -> 通过。 + - `cd web && npx eslint src/app/admin/todos/page.tsx src/types/auth.ts` -> 通过。 + +- 风险与影响: + - 表名从 `todos` 切到 `todo`,若现网已落 `todos` 数据,需做数据迁移或并行兼容策略;当前实现按“以 quiz 表为准”直接切换。 + - `init-mindmap` 当前仅返回初始化结构(fquiz 目前无独立 mindmap 存储与编辑链路),页面侧做成功反馈,不做跳转编辑。 + +## Work Log - 思维导图按 quiz 表与逻辑落地(2026-04-22) + +- 背景:按“用老工程表,参考老工程逻辑改造当前工程思维导图功能”要求,将 `fquiz` 的 `/admin/mindmap` 从题库统计复用页切回独立思维导图模块。 + +- 本次改动(最小闭环): + - 后端模型与路由新增: + - `api/app/models/mind_map.py` + - 新增老工程口径主表 `mind_map`(字段:`id/map_name/descr/map_data/create_date/create_user/update_date/update_user`)。 + - 索引对齐:`idx_mind_map_name`、`idx_mind_map_create_date`。 + - `api/app/schemas/mind_map.py` + - 新增 `MindMapQueryRequest/MindMapCreateRequest/MindMapBasicInfoUpdateRequest/MindMapDataUpdateRequest`。 + - 新增 `MindMapSummary/MindMapPageResponse`。 + - `api/app/services/mind_map_service.py` + - 新增 search/get/create/update-basic-info/update-data/delete 业务逻辑。 + - 新增 AI 流式生成:`stream_generate_mind_map`,输出 `[PARSE_RESULT]` + `[MINDMAP]{json}` 标记。 + - 新增初始化模板构造:`build_initial_mind_map_data(title)`。 + - `api/app/api/v1/mind_map.py` + - 新增接口: + - `POST /api/v1/mindmap/search` + - `GET /api/v1/mindmap/get/{id}` + - `POST /api/v1/mindmap/create` + - `PUT /api/v1/mindmap/update-basic-info` + - `PUT /api/v1/mindmap/update-data` + - `DELETE /api/v1/mindmap/delete/{id}` + - `GET /api/v1/mindmap/generate/stream` + - 读接口权限:`question_bank.read | question_bank.manage`; + - 写接口权限:`question_bank.manage`。 + - `api/app/api/router.py`、`api/app/core/database.py`、`api/app/models/__init__.py` + - 注册 `mind_map` 模块与路由,确保启动建表与路由生效。 + + - Todo 联动改造: + - `api/app/services/todo_service.py` + - `init_todo_mindmap` 从“仅返回初始化 JSON”改为“真实落库到 `mind_map`”: + - 若同 `todo.id` 的导图存在则直接返回; + - 不存在则按老逻辑以 `todo.id` 创建导图并返回。 + - `web/src/app/admin/todos/page.tsx` + - `分析` 后改为跳转 `/admin/mindmap/edit/{id}`,与老工程链路一致。 + + - 前端页面切换为独立思维导图: + - `web/src/app/admin/question-bank/page.tsx` + - 固化为原题库页面实现(不再从 `../mindmap/page` 复用导出)。 + - `web/src/app/admin/mindmap/page.tsx` + - 改为思维导图列表页:查询、分页、新建、编辑基础信息、删除、进入绘图页。 + - `web/src/app/admin/mindmap/_components/mindmap-editor.tsx` + - 新增编辑器:基础信息编辑、JSON 编辑、树预览、保存、导出 JSON/Markdown、AI 流式生成。 + - `web/src/app/admin/mindmap/edit/page.tsx` + - `web/src/app/admin/mindmap/edit/[id]/page.tsx` + - 新增新建/编辑路由承载页。 + - `web/src/types/auth.ts` + - 新增 `MindMapSummary/MindMapListResponse` 类型。 + + - 菜单与入口文案对齐: + - `api/app/services/seed_service.py` + - `admin.mindmap` 菜单名从“题库统计”改为“思维导图”。 + - `web/src/app/admin/page.tsx` + - 后台首页入口文案改为“思维导图”。 + +- 验证: + - `python3 -m py_compile api/app/models/mind_map.py api/app/schemas/mind_map.py api/app/services/mind_map_service.py api/app/api/v1/mind_map.py api/app/services/todo_service.py api/app/api/router.py api/app/core/database.py api/app/models/__init__.py` -> 通过。 + - `python3 -m compileall api/app` -> 通过。 + - 前端 lint/build 未完成: + - `npx eslint ...` 受当前环境 npm 依赖解析影响失败(`Cannot find module 'next/dist/compiled/babel-packages'`); + - `npm run build:web` 受当前环境缺失 `next` 可执行文件影响失败(`next: not found`)。 + +- 风险与影响: + - 当前编辑页采用 JSON 编辑 + 树预览实现,未引入老工程 `mind-elixir` 可视化拖拽编辑能力;若后续需要“节点拖拽/工具栏”体验,需单独引入前端图库并做适配。 + - 思维导图分组/标签/向量同步链路(老工程中的 group/tag/vector)本次未迁入,当前闭环聚焦 `mind_map` 主表与 Todo 联动。 + +## Work Log - Mermaid 按 quiz 表与页面重构(2026-04-22) + +- 背景:按“改用 quiz 的表,照抄 guiz/quiz 逻辑和页面,重新改造 mermaid 功能”要求,将 `fquiz` 的 Mermaid 模块从“菜单复用 mdresolve”升级为独立 Mermaid 管理与编辑链路。 + +- 本次改动(最小闭环): + - 后端 Mermaid 接口落地(对齐 quiz 路径语义) + - 新增 `api/app/api/v1/mermaids.py`: + - `POST /api/v1/mermaids/diagrams/search` + - `GET /api/v1/mermaids/diagrams/groups` + - `GET /api/v1/mermaids/diagrams/get/{id}` + - `POST /api/v1/mermaids/diagrams/create` + - `PUT /api/v1/mermaids/diagrams/update` + - `DELETE /api/v1/mermaids/diagrams/delete/{id}` + - `PATCH /api/v1/mermaids/diagrams/{id}/data` + - `GET /api/v1/mermaids/diagrams/generate/stream` + - `POST /api/v1/mermaids/diagrams/chat/stream` + - `api/app/api/router.py` 挂载 `mermaids_router` 到 `/api/v1`,并新增 legacy 兼容前缀 `/api/mermaids/diagrams/*`(对齐老页面直连习惯)。 + - Mermaid schema 兼容 quiz 请求字段 + - `api/app/schemas/mermaid.py`:为请求 DTO 增加 `validation_alias`,兼容 `camelCase` 与 `snake_case`(如 `diagramName/diagram_name`、`pageNum/page_num`、`diagramData/diagram_data`、`modelName/model_name`)。 + - Mermaid service 修正与增强 + - `api/app/services/mermaid_service.py` + - 新增 `get_mermaid_diagram_summary`,统一单条详情序列化。 + - `create/update/patch-data` 统一复用 summary 返回。 + - 修复流式聊天系统提示组装缺陷(历史版本曾出现重复关键字问题)。 + - 前端页面重构(按 quiz 交互拆分管理页 + 编辑页) + - `web/src/app/admin/mermaid-mgr/page.tsx` + - 从 `mdresolve` 复用页改为独立 Mermaid 管理页。 + - 支持关键字/分组筛选、分页、新建、编辑、删除、详情预览、进入绘图。 + - `web/src/app/admin/mermaid-mgr/[id]/page.tsx` + - 新增 Mermaid 编辑路由。 + - `web/src/app/admin/mermaid-mgr/_components/mermaid-editor.tsx` + - 左侧 AI 对话流式改图,右侧预览,抽屉源码编辑,保存回写 `PATCH /{id}/data`。 + - `web/src/components/mermaid-viewer.tsx` + - 新增 Mermaid 预览组件(通过 `mermaid.ink` SVG 渲染,避免本地依赖安装阻塞)。 + - 前端类型补齐 + - `web/src/types/auth.ts` 新增 Mermaid 相关类型:分组、图表摘要、分页响应、聊天消息。 + +- 验证: + - `python3 -m py_compile api/app/schemas/mermaid.py api/app/models/mermaid_diagram.py api/app/models/object_group.py api/app/services/mermaid_service.py api/app/api/v1/mermaids.py api/app/api/router.py` -> 通过。 + - 前端 ESLint 未执行成功:当前环境 `eslint` 二进制缺失(`sh: 1: eslint: not found`),且 `npm --workspace web install` 过程中出现大量 tarball 校验重试,未形成可用依赖环境。 + +- 风险与影响: + - Mermaid 预览当前走 `mermaid.ink` 在线渲染;若运行环境外网受限,预览图可能加载失败,但源码编辑与保存链路不受影响。 + - 当前工作区存在大量历史未提交改动;本次仅在 Mermaid 相关文件范围内追加改造,未处理既有脏改动。 + +## Work Log - 日程管理按 quiz 表与页面重构(2026-04-22) + +- 背景:按“改用 quiz 的表结构,照抄 quiz 的逻辑和页面”要求,把 `fquiz` 的日程管理从“菜单复用 todo 页面”升级为独立日程模块。 + +- 本次改动(最小闭环): + - 后端表结构与接口落地: + - 新增 `api/app/models/calendar_event.py`,表名 `calendar_event`,字段对齐 quiz:`title/descr/status/priority/start_time/end_time/expire_time/all_day/completed_at/todo_id/create_date/create_user/update_date/update_user`。 + - 新增 `api/app/schemas/calendar_event.py`(查询/创建/更新/分页响应 DTO)。 + - 新增 `api/app/services/calendar_event_service.py`(查询、增改删、完成、AI 流式生成、过期自动置 `EXPIRED`、与 todo 双向同步)。 + - 新增 `api/app/api/v1/calendar.py`,提供: + - `POST /api/v1/calendar/search` + - `GET /api/v1/calendar/get/{id}` + - `POST /api/v1/calendar/create` + - `PUT /api/v1/calendar/update` + - `DELETE /api/v1/calendar/delete/{id}` + - `POST /api/v1/calendar/{id}/complete` + - `GET /api/v1/calendar/generate/stream` + - `api/app/api/router.py` 挂载 `calendar_router`;`api/app/models/__init__.py` 与 `api/app/core/database.py` 增加 `calendar_event` 模型注册。 + - Todo 双向同步补齐: + - `api/app/schemas/todo.py` 为 `TodoUpdateRequest/TodoTransitionRequest` 增加 `is_sync` 标记。 + - `api/app/services/todo_service.py` 的 `create/update/transition/complete/delete` 增加 `syncing` 参数并接入 `calendar_event_service` 同步函数,防止 schedule<->todo 循环调用。 + - 前端页面改造: + - `web/src/app/admin/schedule/page.tsx` 从复用导出改为独立页面,实现 quiz 风格年/月/周视图、创建/编辑/删除/完成、AI 流式生成并回填表单。 + +- 验证: + - `python3 -m compileall api/app/models/calendar_event.py api/app/schemas/calendar_event.py api/app/services/calendar_event_service.py api/app/api/v1/calendar.py api/app/services/todo_service.py api/app/schemas/todo.py api/app/api/v1/todos.py` -> 通过。 + - 前端 lint/build 未完全通过环境验证: + - `cd web && npx eslint src/app/admin/schedule/page.tsx src/types/auth.ts` -> 失败(`eslint: not found`)。 + - `npm --workspace web install` 持续出现 `next` 解包 `TAR_ENTRY_ERROR ENOENT`,依赖环境未恢复,暂无法给出有效前端构建结论。 + +- 风险与影响: + - 依赖环境异常会阻断前端 lint/build 验证,但不影响本次后端与页面代码改造落地。 + - 当前工作区存在大量历史脏改动;本次仅在日程与 todo 相关范围内追加改造,未触及其余模块。 diff --git a/memory/2026-04-23.md b/memory/2026-04-23.md new file mode 100644 index 0000000..0de7fd0 --- /dev/null +++ b/memory/2026-04-23.md @@ -0,0 +1,58 @@ +## Work Log - 日记管理按 quiz 表与逻辑重构(2026-04-23) + +- 背景:按“用老工程表,参考老工程逻辑改造当前工程日记管理功能”要求,将 `fquiz` 的 `/admin/diary` 从系统日志复用页切换为独立 Diary 模块。 + +- 本次改动(最小闭环): + - 后端模型与接口落地(对齐 quiz 口径): + - `api/app/models/diary.py` + - 新增主表 `diary`,字段: + - `id/title/content/diary_date/mood/weather/archived/create_date/create_user/update_date/update_user` + - 索引:`idx_diary_create_user/idx_diary_diary_date/idx_diary_mood/idx_diary_archived`。 + - `api/app/schemas/diary.py` + - 新增 `DiaryMood` 枚举:`HAPPY/CALM/SAD/ANGRY/TIRED/EXCITED`。 + - 新增 `DiaryQueryRequest/DiaryCreateRequest/DiaryUpdateRequest/DiarySummary/DiaryPageResponse`。 + - `api/app/services/diary_service.py` + - 新增 create/search/get/update/delete/archive 逻辑。 + - 查询逻辑对齐 quiz:按 `create_user` 隔离数据,支持 `title/mood/diary_date_start/diary_date_end/archived` 过滤,排序 `diary_date DESC, create_date DESC`。 + - 归档逻辑对齐 quiz:`archive` 仅修改 `archived` 状态,保留原记录。 + - `api/app/api/v1/diary.py` + - 新增接口: + - `POST /api/v1/diary/search` + - `GET /api/v1/diary/get/{id}` + - `POST /api/v1/diary/create` + - `PUT /api/v1/diary/update` + - `DELETE /api/v1/diary/delete/{id}` + - `POST /api/v1/diary/{id}/archive?archived=...` + - 权限口径: + - 读:`menu.read | menu.manage` + - 写:`menu.manage` + - 路由与建表注册: + - `api/app/api/router.py` 挂载 `diary` 路由。 + - `api/app/models/__init__.py`、`api/app/core/database.py` 注册 `diary` 模型,确保 `create_all` 生效。 + + - 前端页面改造(替换 syslog 复用): + - `web/src/app/admin/diary/page.tsx` + - 改为独立 Diary 页面: + - 列表分页 + - 条件查询(标题、心情、日期范围、归档状态) + - 新增 / 编辑 / 删除 + - 归档 / 取消归档 + - 详情弹窗 + - API 调整到新后端接口 `/api/v1/diary/*`。 + - `web/src/types/auth.ts` + - 新增 `DiaryMood/DiarySummary/DiaryListResponse` 类型。 + - `web/src/app/admin/page.tsx` + - 首页卡片文案从“上帝视角”调整为“日记管理”。 + - `api/app/services/seed_service.py` + - 菜单 `admin.diary` 名称更新为“日记管理”(路径保持 `/admin/diary`)。 + +- 验证: + - `python3 -m py_compile api/app/models/diary.py api/app/schemas/diary.py api/app/services/diary_service.py api/app/api/v1/diary.py api/app/api/router.py api/app/models/__init__.py api/app/core/database.py api/app/services/seed_service.py` -> 通过。 + - `python3 -m compileall api/app` -> 通过。 + - 前端 ESLint 未完成(环境依赖缺失): + - `npm run lint` 提示 `eslint: not found`; + - 直接执行 eslint 后提示缺少 `typescript` 依赖(`Cannot find module 'typescript'`)。 + +- 风险与影响: + - `admin.diary` 保留 `menu.read/menu.manage` 权限体系(未引入独立 `diary.read/diary.manage`),属于兼容方案;后续若需要更细权限边界,可单独拆分权限码。 + - 本次仅对齐了老工程 diary 主链路,不包含分组/tag 维度扩展(老工程该模块本身也未强依赖)。 diff --git a/package-lock.json b/package-lock.json index 45d4354..1a3f8c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,1572 +11,269 @@ "web" ] }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@ant-design/fast-color": "^2.0.6" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react": ">=16.0.0", + "react-dom": ">=16.0.0" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@radix-ui/colors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", - "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", - "license": "MIT" - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { + "node_modules/@ant-design/cssinjs-utils": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", "license": "MIT" }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", - "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", - "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", - "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { + "node_modules/@ant-design/react-slick": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", - "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": ">=16.9.0" } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@babel/runtime": "^7.24.4" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=14.x" } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@radix-ui/react-focus-guards": { + "node_modules/@rc-component/mini-decimal": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@babel/runtime": "^7.18.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8.x" } }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", - "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", - "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", - "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", - "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", - "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", - "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.11" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@radix-ui/themes": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/themes/-/themes-3.3.0.tgz", - "integrity": "sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw==", - "license": "MIT", - "dependencies": { - "@radix-ui/colors": "^3.0.0", + "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", - "radix-ui": "^1.1.3", - "react-remove-scroll-bar": "^2.3.8" + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz", + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/@tanstack/query-core": { @@ -1601,16 +298,69 @@ "react": "^18 || ^19" } }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "node_modules/antd": { + "version": "5.29.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", + "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.1.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.1", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.54.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.11.0", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/class-variance-authority": { @@ -1646,10 +396,31 @@ "node": ">=6" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/framer-motion": { @@ -1679,13 +450,13 @@ } } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "string-convert": "^0.2.0" } }, "node_modules/motion-dom": { @@ -1703,83 +474,612 @@ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", "license": "MIT" }, - "node_modules/radix-ui": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", - "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-accessible-icon": "1.1.7", - "@radix-ui/react-accordion": "1.2.12", - "@radix-ui/react-alert-dialog": "1.1.15", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-aspect-ratio": "1.1.7", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-context-menu": "2.2.16", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-form": "0.1.8", - "@radix-ui/react-hover-card": "1.1.15", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-menubar": "1.1.16", - "@radix-ui/react-navigation-menu": "1.2.14", - "@radix-ui/react-one-time-password-field": "0.1.8", - "@radix-ui/react-password-toggle-field": "0.1.3", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-progress": "1.1.7", - "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-scroll-area": "1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slider": "1.3.6", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-toolbar": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-escape-keydown": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz", + "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz", + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" }, "peerDependenciesMeta": { - "@types/react": { + "date-fns": { "optional": true }, - "@types/react-dom": { + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { "optional": true } } }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz", + "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz", + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz", + "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "19.2.4", "license": "MIT", @@ -1799,74 +1099,17 @@ "react": "^19.2.4" } }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" }, "node_modules/scheduler": { "version": "0.27.0", @@ -1874,6 +1117,27 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", @@ -1881,64 +1145,27 @@ "dev": true, "license": "MIT" }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "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/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/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/web": { "resolved": "web", "link": true @@ -1946,10 +1173,8 @@ "web": { "version": "0.1.0", "dependencies": { - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/themes": "^3.3.0", "@tanstack/react-query": "^5.90.5", + "antd": "^5.29.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", @@ -3332,11 +2557,6 @@ "node": ">= 8" } }, - "web/node_modules/csstype": { - "version": "3.2.3", - "dev": true, - "license": "MIT" - }, "web/node_modules/damerau-levenshtein": { "version": "1.0.8", "dev": true, diff --git a/web/Dockerfile b/web/Dockerfile index 4a3c83e..a3b6093 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -4,6 +4,7 @@ FROM ${NODE_BASE_IMAGE} AS deps WORKDIR /app COPY package.json package-lock.json ./ +RUN npm config set registry https://mirrors.cloud.tencent.com/npm/ RUN npm ci FROM ${NODE_BASE_IMAGE} AS builder diff --git a/web/package-lock.json b/web/package-lock.json index 1722951..f5ab020 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,10 +8,8 @@ "name": "web", "version": "0.1.0", "dependencies": { - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/themes": "^3.3.0", "@tanstack/react-query": "^5.90.5", + "antd": "^5.29.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", @@ -42,6 +40,103 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "dev": true, @@ -208,6 +303,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "dev": true, @@ -250,6 +354,18 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "dev": true, @@ -374,44 +490,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, "node_modules/@humanfs/core": { "version": "0.19.1", "dev": true, @@ -716,1534 +794,151 @@ "node": ">=12.4.0" } }, - "node_modules/@radix-ui/colors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", - "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", - "license": "MIT" + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } }, - "node_modules/@radix-ui/primitive": { + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", - "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", "license": "MIT", "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.3" + "@babel/runtime": "^7.18.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8.x" } }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", - "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", - "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", - "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", - "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", - "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", - "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", - "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", - "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", - "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.11" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@radix-ui/themes": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/themes/-/themes-3.3.0.tgz", - "integrity": "sha512-I0/h2CRNTpYNB7Mi3xFIvSsQq5a108d7kK8dTO5zp5b9HR5QJXKag6B8tjpz2ITkVYkFdkGk45doNkSr7OxwNw==", - "license": "MIT", - "dependencies": { - "@radix-ui/colors": "^3.0.0", + "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", - "radix-ui": "^1.1.3", - "react-remove-scroll-bar": "^2.3.8" + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz", + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/@rtsao/scc": { @@ -2387,7 +1082,7 @@ }, "node_modules/@types/react": { "version": "19.2.14", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2395,7 +1090,7 @@ }, "node_modules/@types/react-dom": { "version": "19.2.3", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2724,23 +1419,76 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd": { + "version": "5.29.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", + "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.1.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.1", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.54.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.11.0", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, "license": "Python-2.0" }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/aria-query": { "version": "5.3.2", "dev": true, @@ -3137,6 +1885,12 @@ "dev": true, "license": "MIT" }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -3147,6 +1901,15 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -3162,7 +1925,6 @@ }, "node_modules/csstype": { "version": "3.2.3", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -3218,6 +1980,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "dev": true, @@ -3279,12 +2047,6 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/doctrine": { "version": "2.1.0", "dev": true, @@ -4095,15 +2857,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-proto": { "version": "1.0.1", "dev": true, @@ -4770,6 +3523,15 @@ "dev": true, "license": "MIT" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -5438,83 +4200,618 @@ ], "license": "MIT" }, - "node_modules/radix-ui": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", - "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-accessible-icon": "1.1.7", - "@radix-ui/react-accordion": "1.2.12", - "@radix-ui/react-alert-dialog": "1.1.15", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-aspect-ratio": "1.1.7", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-context-menu": "2.2.16", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-form": "0.1.8", - "@radix-ui/react-hover-card": "1.1.15", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-menubar": "1.1.16", - "@radix-ui/react-navigation-menu": "1.2.14", - "@radix-ui/react-one-time-password-field": "0.1.8", - "@radix-ui/react-password-toggle-field": "0.1.3", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-progress": "1.1.7", - "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-scroll-area": "1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slider": "1.3.6", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-toolbar": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-escape-keydown": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz", + "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz", + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" }, "peerDependenciesMeta": { - "@types/react": { + "date-fns": { "optional": true }, - "@types/react-dom": { + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { "optional": true } } }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz", + "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz", + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz", + "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5541,75 +4838,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "dev": true, @@ -5650,6 +4878,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.6", "dev": true, @@ -5774,6 +5008,15 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "6.3.1", "dev": true, @@ -5990,6 +5233,12 @@ "node": ">= 0.4" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "dev": true, @@ -6131,6 +5380,12 @@ } } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -6180,6 +5435,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "dev": true, @@ -6233,6 +5497,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "dev": true, @@ -6479,58 +5749,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/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/which": { "version": "2.0.2", "dev": true, diff --git a/web/package.json b/web/package.json index 23a57ee..968d200 100644 --- a/web/package.json +++ b/web/package.json @@ -9,10 +9,8 @@ "lint": "eslint" }, "dependencies": { - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/themes": "^3.3.0", "@tanstack/react-query": "^5.90.5", + "antd": "^5.29.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", diff --git a/web/src/app/admin/chat/page.tsx b/web/src/app/admin/chat/page.tsx index 6425233..62f86ef 100644 --- a/web/src/app/admin/chat/page.tsx +++ b/web/src/app/admin/chat/page.tsx @@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ChangeEvent, FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { useAuth } from "@/components/auth-provider"; -import { TextArea, Button } from "@radix-ui/themes"; +import { TextArea, Button } from "@/components/ui-antd"; import { readApiError } from "@/lib/api"; import type { ChatMessage, diff --git a/web/src/app/admin/diary/page.tsx b/web/src/app/admin/diary/page.tsx index ce2e7ef..d829447 100644 --- a/web/src/app/admin/diary/page.tsx +++ b/web/src/app/admin/diary/page.tsx @@ -1 +1,610 @@ -export { default } from "@/app/admin/syslog/page"; +"use client"; + +import Link from "next/link"; +import dayjs, { Dayjs } from "dayjs"; +import { useCallback, useEffect, useState } from "react"; +import { + Button, + DatePicker, + Descriptions, + Form, + Input, + Modal, + Popconfirm, + Select, + Space, + Table, + Tag, + Tooltip, + message, +} from "antd"; +import type { ColumnsType } from "antd/es/table"; + +import { useAuth } from "@/components/auth-provider"; +import { readApiError } from "@/lib/api"; +import type { + DiaryListResponse, + DiaryMood, + DiarySummary, +} from "@/types/auth"; + +type SearchParams = { + title: string; + mood: DiaryMood | ""; + archived: boolean | null; + diary_date_start: string | null; + diary_date_end: string | null; +}; + +type SearchFormValues = { + title?: string; + mood?: DiaryMood; + archived?: "all" | "active" | "archived"; + diary_date_range?: [Dayjs, Dayjs] | null; +}; + +type DiaryFormValues = { + title: string; + content: string; + diary_date: Dayjs; + mood: DiaryMood; + weather?: string; +}; + +type PaginationState = { + current: number; + pageSize: number; + total: number; +}; + +const DEFAULT_SEARCH: SearchParams = { + title: "", + mood: "", + archived: null, + diary_date_start: null, + diary_date_end: null, +}; + +const DEFAULT_PAGINATION: PaginationState = { + current: 1, + pageSize: 20, + total: 0, +}; + +const MOOD_OPTIONS: Array<{ label: string; value: DiaryMood }> = [ + { label: "开心", value: "HAPPY" }, + { label: "平静", value: "CALM" }, + { label: "难过", value: "SAD" }, + { label: "生气", value: "ANGRY" }, + { label: "疲惫", value: "TIRED" }, + { label: "兴奋", value: "EXCITED" }, +]; + +const MOOD_TAG_MAP: Record = { + HAPPY: { color: "green", text: "开心" }, + CALM: { color: "blue", text: "平静" }, + SAD: { color: "purple", text: "难过" }, + ANGRY: { color: "red", text: "生气" }, + TIRED: { color: "default", text: "疲惫" }, + EXCITED: { color: "orange", text: "兴奋" }, +}; + +function formatDate(value: string | null): string { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "-"; + return date.toLocaleDateString(); +} + +function formatDateTime(value: string | null): string { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "-"; + return date.toLocaleString(); +} + +export default function AdminDiaryPage() { + const { user, initializing, fetchWithAuth, hasPermission } = useAuth(); + + const [searchForm] = Form.useForm(); + const [addForm] = Form.useForm(); + const [editForm] = Form.useForm(); + + const [searchParams, setSearchParams] = useState(DEFAULT_SEARCH); + const [pagination, setPagination] = useState(DEFAULT_PAGINATION); + + const [tableData, setTableData] = useState([]); + const [tableLoading, setTableLoading] = useState(false); + + const [addVisible, setAddVisible] = useState(false); + const [editVisible, setEditVisible] = useState(false); + const [detailVisible, setDetailVisible] = useState(false); + const [saving, setSaving] = useState(false); + + const [currentRecord, setCurrentRecord] = useState(null); + const [panelError, setPanelError] = useState(""); + + const canRead = hasPermission("menu.read") || hasPermission("menu.manage"); + const canManage = hasPermission("menu.manage"); + + const fetchTableData = useCallback( + async (params: SearchParams, pageSize: number, current: number) => { + setTableLoading(true); + try { + const response = await fetchWithAuth("/api/v1/diary/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title: params.title || null, + mood: params.mood || null, + diary_date_start: params.diary_date_start, + diary_date_end: params.diary_date_end, + archived: params.archived, + page_num: Math.max(0, current - 1), + page_size: pageSize, + }), + }); + if (!response.ok) { + throw new Error(await readApiError(response)); + } + + const payload = (await response.json()) as DiaryListResponse; + setTableData(payload.items ?? []); + setPagination((prev) => ({ + ...prev, + current, + pageSize, + total: payload.total ?? 0, + })); + setPanelError(""); + } catch (error) { + const text = error instanceof Error ? error.message : "获取日记列表失败"; + setPanelError(text); + } finally { + setTableLoading(false); + } + }, + [fetchWithAuth], + ); + + useEffect(() => { + if (!user || !canRead) return; + void fetchTableData(searchParams, pagination.pageSize, pagination.current); + }, [canRead, fetchTableData, pagination.current, pagination.pageSize, searchParams, user]); + + const handleSearch = (values: SearchFormValues) => { + const range = values.diary_date_range; + let archived: boolean | null = null; + if (values.archived === "active") archived = false; + if (values.archived === "archived") archived = true; + + const nextParams: SearchParams = { + title: (values.title ?? "").trim(), + mood: values.mood ?? "", + archived, + diary_date_start: range?.[0] ? range[0].format("YYYY-MM-DD") : null, + diary_date_end: range?.[1] ? range[1].format("YYYY-MM-DD") : null, + }; + + setSearchParams(nextParams); + setPagination((prev) => ({ ...prev, current: 1 })); + }; + + const handleReset = () => { + searchForm.resetFields(); + setSearchParams(DEFAULT_SEARCH); + setPagination((prev) => ({ ...prev, current: 1 })); + }; + + const handlePaginationChange = (current: number, pageSize: number) => { + setPagination((prev) => ({ ...prev, current, pageSize })); + }; + + const openAddModal = () => { + addForm.resetFields(); + addForm.setFieldsValue({ + diary_date: dayjs(), + mood: "CALM", + }); + setAddVisible(true); + }; + + const openEditModal = (record: DiarySummary) => { + setCurrentRecord(record); + editForm.setFieldsValue({ + title: record.title, + content: record.content, + diary_date: dayjs(record.diary_date), + mood: record.mood, + weather: record.weather ?? undefined, + }); + setEditVisible(true); + }; + + const openDetailModal = (record: DiarySummary) => { + setCurrentRecord(record); + setDetailVisible(true); + }; + + const handleAddConfirm = async () => { + try { + const values = await addForm.validateFields(); + setSaving(true); + + const response = await fetchWithAuth("/api/v1/diary/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title: values.title, + content: values.content, + diary_date: values.diary_date.format("YYYY-MM-DD"), + mood: values.mood, + weather: values.weather ?? null, + archived: false, + }), + }); + if (!response.ok) { + throw new Error(await readApiError(response)); + } + + message.success("日记创建成功"); + setAddVisible(false); + addForm.resetFields(); + await fetchTableData(searchParams, pagination.pageSize, pagination.current); + } catch (error) { + if (error instanceof Error && error.message.includes("out of date")) return; + const text = error instanceof Error ? error.message : "日记创建失败"; + message.error(text); + setPanelError(text); + } finally { + setSaving(false); + } + }; + + const handleEditConfirm = async () => { + if (!currentRecord) return; + try { + const values = await editForm.validateFields(); + setSaving(true); + + const response = await fetchWithAuth("/api/v1/diary/update", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: currentRecord.id, + title: values.title, + content: values.content, + diary_date: values.diary_date.format("YYYY-MM-DD"), + mood: values.mood, + weather: values.weather ?? null, + archived: currentRecord.archived, + }), + }); + if (!response.ok) { + throw new Error(await readApiError(response)); + } + + message.success("日记更新成功"); + setEditVisible(false); + setCurrentRecord(null); + await fetchTableData(searchParams, pagination.pageSize, pagination.current); + } catch (error) { + if (error instanceof Error && error.message.includes("out of date")) return; + const text = error instanceof Error ? error.message : "日记更新失败"; + message.error(text); + setPanelError(text); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (record: DiarySummary) => { + try { + const response = await fetchWithAuth(`/api/v1/diary/delete/${record.id}`, { + method: "DELETE", + }); + if (!response.ok) { + throw new Error(await readApiError(response)); + } + message.success("日记删除成功"); + await fetchTableData(searchParams, pagination.pageSize, pagination.current); + } catch (error) { + const text = error instanceof Error ? error.message : "日记删除失败"; + message.error(text); + setPanelError(text); + } + }; + + const handleArchiveToggle = async (record: DiarySummary) => { + try { + const response = await fetchWithAuth(`/api/v1/diary/${record.id}/archive?archived=${String(!record.archived)}`, { + method: "POST", + }); + if (!response.ok) { + throw new Error(await readApiError(response)); + } + message.success(record.archived ? "已取消归档" : "已归档"); + await fetchTableData(searchParams, pagination.pageSize, pagination.current); + } catch (error) { + const text = error instanceof Error ? error.message : "归档操作失败"; + message.error(text); + setPanelError(text); + } + }; + + const columns: ColumnsType = [ + { + title: "日期", + dataIndex: "diary_date", + width: 140, + render: (value: string) => formatDate(value), + }, + { + title: "标题", + dataIndex: "title", + ellipsis: true, + render: (_: string, record) => ( + + ), + }, + { + title: "心情", + dataIndex: "mood", + width: 110, + align: "center", + render: (mood: DiaryMood) => { + const mapped = MOOD_TAG_MAP[mood]; + return {mapped.text}; + }, + }, + { + title: "天气", + dataIndex: "weather", + width: 120, + render: (value: string | null) => value || "-", + }, + { + title: "状态", + dataIndex: "archived", + width: 110, + align: "center", + render: (archived: boolean) => ( + {archived ? "已归档" : "使用中"} + ), + }, + { + title: "更新时间", + dataIndex: "update_date", + width: 180, + render: (value: string | null) => formatDateTime(value), + }, + { + title: "操作", + width: 220, + align: "center", + render: (_, record) => ( + + + + + + void handleArchiveToggle(record)} + > + + + + + + void handleDelete(record)}> + + + + + + ), + }, + ]; + + if (initializing) { + return

Loading diaries...

; + } + + if (!user) { + return ( +
+

请先登录后再访问日记管理页面。

+ + 返回首页 + +
+ ); + } + + if (!canRead) { + return

缺少 `menu.read` 或 `menu.manage` 权限。

; + } + + return ( +
+
+
+
+

日记管理

+

按老工程逻辑管理日记:查询、新增、编辑、归档与详情查看。

+
+ + + 总数 {pagination.total} + +
+ + + form={searchForm} + layout="inline" + initialValues={{ archived: "all" }} + onFinish={handleSearch} + > + + + + + + + + + + + + + +
+ + {panelError ? ( +
+ {panelError} +
+ ) : null} + +
+ + rowKey="id" + loading={tableLoading} + dataSource={tableData} + columns={columns} + pagination={{ + current: pagination.current, + pageSize: pagination.pageSize, + total: pagination.total, + showSizeChanger: true, + showQuickJumper: true, + onChange: handlePaginationChange, + }} + /> +
+ + setAddVisible(false)} + onOk={() => void handleAddConfirm()} + confirmLoading={saving} + destroyOnHidden + > + form={addForm} layout="vertical"> + + + + + + + + + + + + + + + + { + setEditVisible(false); + setCurrentRecord(null); + }} + onOk={() => void handleEditConfirm()} + confirmLoading={saving} + destroyOnHidden + > + form={editForm} layout="vertical"> + + + + + + + + + + + + + + + + { + setDetailVisible(false); + setCurrentRecord(null); + }} + > + {currentRecord && ( +
+ + {formatDate(currentRecord.diary_date)} + {MOOD_TAG_MAP[currentRecord.mood].text} + + {currentRecord.archived ? "已归档" : "使用中"} + + + + + {currentRecord.weather || "-"} + {currentRecord.create_user || "-"} + {formatDateTime(currentRecord.update_date)} + + +
+ {currentRecord.content} +
+
+ )} +
+
+ ); +} diff --git a/web/src/app/admin/files/page.tsx b/web/src/app/admin/files/page.tsx index e5b1da2..92296e5 100644 --- a/web/src/app/admin/files/page.tsx +++ b/web/src/app/admin/files/page.tsx @@ -6,7 +6,7 @@ import { useCallback, useMemo, useState } from "react"; import type { ChangeEvent } from "react"; import { useAuth } from "@/components/auth-provider"; -import { Button, Table, TextField } from "@radix-ui/themes"; +import { Button, Table, TextField } from "@/components/ui-antd"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import type { diff --git a/web/src/app/admin/hot-search/page.tsx b/web/src/app/admin/hot-search/page.tsx index 05732e2..aca48cf 100644 --- a/web/src/app/admin/hot-search/page.tsx +++ b/web/src/app/admin/hot-search/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from "react"; -import { Button, Dialog, Select, Table, TextArea, TextField } from "@radix-ui/themes"; +import { Button, Dialog, Select, Table, TextArea, TextField } from "@/components/ui-antd"; import { useAuth } from "@/components/auth-provider"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; diff --git a/web/src/app/admin/jwt-generator/page.tsx b/web/src/app/admin/jwt-generator/page.tsx index ba385c9..16bfd6c 100644 --- a/web/src/app/admin/jwt-generator/page.tsx +++ b/web/src/app/admin/jwt-generator/page.tsx @@ -5,7 +5,7 @@ import { ChangeEvent, useCallback, useState } from "react"; import { useAuth } from "@/components/auth-provider"; import { readApiError } from "@/lib/api"; -import { Button, TextField } from "@radix-ui/themes"; +import { Button, TextField } from "@/components/ui-antd"; type JwtGenerateResponse = { token_type: string; diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index 436d28d..735699c 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -8,7 +8,7 @@ import { useAuth } from "@/components/auth-provider"; import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import type { MenuTreeItem } from "@/types/auth"; -import { Button, Callout, Card, Flex, Heading, Text } from "@radix-ui/themes"; +import { Button, Callout, Card, DropdownMenu, Flex, Heading, Text } from "@/components/ui-antd"; function flattenMenuTree(tree: MenuTreeItem[]): MenuTreeItem[] { const result: MenuTreeItem[] = []; @@ -24,6 +24,31 @@ function flattenMenuTree(tree: MenuTreeItem[]): MenuTreeItem[] { return result; } +type MenuPathItem = { + id: number; + name: string; + path: string; + depth: number; +}; + +function flattenMenuPaths(tree: MenuTreeItem[], depth = 0): MenuPathItem[] { + const result: MenuPathItem[] = []; + for (const item of tree) { + if (item.path) { + result.push({ + id: item.id, + name: item.name, + path: item.path, + depth, + }); + } + if (item.children.length > 0) { + result.push(...flattenMenuPaths(item.children, depth + 1)); + } + } + return result; +} + function isActivePath(pathname: string, menuPath: string | null): boolean { if (!menuPath) { return false; @@ -99,6 +124,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) }, [loadMenus])); const flatMenus = useMemo(() => flattenMenuTree(menuTree), [menuTree]); + const mobileMenuItems = useMemo(() => flattenMenuPaths(menuTree), [menuTree]); const currentTitle = useMemo(() => { const current = flatMenus.find((item) => isActivePath(pathname, item.path)); return current?.name ?? "后台管理"; @@ -125,7 +151,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) return (
-