From d0f7f0d3d862a0980ac323887923e57b94a6c9b5 Mon Sep 17 00:00:00 2001 From: oneao Date: Thu, 2 Jul 2026 21:54:26 +0800 Subject: [PATCH] 20260702215426 --- .../scripts/__pycache__/core.cpython-313.pyc | Bin 0 -> 11913 bytes .../__pycache__/design_system.cpython-313.pyc | Bin 0 -> 58746 bytes .../documents/clothes_ui_redesign_plan.md | 316 +++ code/app/CODE_STYLE.md | 10 +- code/app/MEAL_ORDER_DESIGN.md | 204 +- code/app/app-go/configs/config.dev.yaml | 2 +- code/app/app-go/db/meal.sql | 227 +- code/app/app-rn/eslint.config.js | 10 + code/app/app-rn/expo-start.err.log | 12 + code/app/app-rn/expo-start.log | 11 + code/app/app-rn/package.json | 2 + code/app/app-rn/pnpm-lock.yaml | 2286 +++++++++++++++++ code/app/app-rn/pnpm-workspace.yaml | 1 + .../src/app/clothes/(tabs)/home/index.tsx | 676 +---- .../src/app/clothes/(tabs)/manage/index.tsx | 842 ++---- .../src/app/clothes/(tabs)/outfit/index.tsx | 452 +--- .../src/app/clothes/detail/[id]/index.tsx | 125 + .../app/app-rn/src/app/clothes/edit/index.tsx | 5 +- .../app/clothes/outfit/detail/[id]/index.tsx | 410 +-- .../src/app/clothes/outfit/edit/index.tsx | 145 +- .../src/app/finance/(tabs)/analysis/index.tsx | 3 +- .../app-rn/src/app/meal/(tabs)/_layout.tsx | 31 + .../src/app/meal/(tabs)/dishes/index.tsx | 436 ++++ .../app-rn/src/app/meal/(tabs)/home/index.tsx | 680 +++++ .../src/app/meal/(tabs)/manage/index.tsx | 775 ++++++ .../src/app/meal/dish/detail/[id]/index.tsx | 297 +++ .../app-rn/src/app/meal/dish/edit/index.tsx | 957 +++++++ .../app-rn/src/app/meal/plan/edit/index.tsx | 596 +++++ .../app/app-rn/src/components/BottomSheet.tsx | 7 +- .../app/app-rn/src/components/ContextMenu.tsx | 43 + .../components/clothing/ClothesGridItem.tsx | 26 + .../clothing/ClothesHorizontalCard.tsx | 39 + .../src/components/iconfont/IconAppMeal.tsx | 50 + .../app-rn/src/components/iconfont/index.tsx | 9 +- .../src/components/meal/MealDishImage.tsx | 46 + code/app/app-rn/src/configs/pages.ts | 9 +- code/app/app-rn/src/store/index.ts | 1 + code/app/app-rn/src/store/meal.ts | 477 ++++ code/app/app-rn/src/types/meal.ts | 172 ++ 39 files changed, 8341 insertions(+), 2049 deletions(-) create mode 100644 code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc create mode 100644 code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc create mode 100644 code/app/.trae/documents/clothes_ui_redesign_plan.md create mode 100644 code/app/app-rn/eslint.config.js create mode 100644 code/app/app-rn/expo-start.err.log create mode 100644 code/app/app-rn/expo-start.log create mode 100644 code/app/app-rn/src/app/clothes/detail/[id]/index.tsx create mode 100644 code/app/app-rn/src/app/meal/(tabs)/_layout.tsx create mode 100644 code/app/app-rn/src/app/meal/(tabs)/dishes/index.tsx create mode 100644 code/app/app-rn/src/app/meal/(tabs)/home/index.tsx create mode 100644 code/app/app-rn/src/app/meal/(tabs)/manage/index.tsx create mode 100644 code/app/app-rn/src/app/meal/dish/detail/[id]/index.tsx create mode 100644 code/app/app-rn/src/app/meal/dish/edit/index.tsx create mode 100644 code/app/app-rn/src/app/meal/plan/edit/index.tsx create mode 100644 code/app/app-rn/src/components/ContextMenu.tsx create mode 100644 code/app/app-rn/src/components/clothing/ClothesGridItem.tsx create mode 100644 code/app/app-rn/src/components/clothing/ClothesHorizontalCard.tsx create mode 100644 code/app/app-rn/src/components/iconfont/IconAppMeal.tsx create mode 100644 code/app/app-rn/src/components/meal/MealDishImage.tsx create mode 100644 code/app/app-rn/src/store/meal.ts create mode 100644 code/app/app-rn/src/types/meal.ts diff --git a/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc b/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a606f368b52f9d2d802d68c675b6c0d6db378fe GIT binary patch literal 11913 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 b2OsSh+P5vA_>B;@I8opK;%}+O)%X7c-D6lA literal 0 HcmV?d00001 diff --git a/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc b/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f569a71c56ff89acb8ba30cec06f40f8f73c065 GIT binary patch literal 58746 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

M@U-ev=l&=)|4G`xP}d$&v&U zoN``F-*yS}NPZR-te#A`(y=7RU+It;d@0OW6|N)-oHG!|D&nfydKE(CbT&@bVy&F% z96DE6CB{VBW$y$av0kO=Il?)tf#m#EC+0OG*=y1hzbEz1s7(J{{ zT)0@`S22gaIq6|GU;98`_RYzkF=(7fmVVqS?kK*JRUCFQ(xtem%RIiKtPyfZCUa!c zh6_6AkmoRv%&5}ItZs?3adnXy4mYVSP zEVwgrd*rKQ>mxr_%d}VJtaX)DuZ1-y+2Jd!dMd2xV+Ss>>PunG0DJN^R(*v$OtR_= zVNDO)XJOUn!kQCo&p4~5!} zFko${KTOpHbcY^J`gMnbsfYZjheBC}@23@lMk-bR_gb0S6f%{0nti6qpvmkrnb}(0 zV>KNQn)-dFe!poDCn}T&W>)3FtZE+`5y6xw>u@mZm@n%X9J95dev+EKRpU=B^IY3j z`>T$GvT8%=_$9r}b7OlFRDwfUHBVBF+~}vCnpy-i)2S(Uwz$dAXLH%-92C6<9eL3# z52wPkUg($ZO1@!7orYQ+eHv=D8Dar1Sy*!G>cQ0kIp~ylz;P$lFS*m8S6zpBIyqJp zD*d;x0)5pjewYXpasXCn4rka^|+L6tP(ag$G<{tod zV06@+JUTi`7V+jP<}o6#!pl5d!bwgSgPjTa@bdhEeVQET#+<^`N#BNHM#kXGnQ6oe zs>(4R;pIQz#f_Jb@$yr+00VM57Z^E4CP$;8%_5Pjk*PG{1f=0H-$iIO`0W5+2Ea3P zaK4I4wGmy=p0 zQ~I#_DA?PB&z>&kw;@~bU*@WZ>CJAkTrPi_k}6j~qm-xqPzL`s|5;Y_pJlm!Ei2ts z%Q7q0bugti6mPxG>Pthqf~~V`dWm?488rD0t+15MtazH5Vn|v~|EX~>S+0F5!|U$3 z6dBCGHMQC_V?lpOzg3BG=F?V=>I;y^oxq{r`eW#bZBNBXPFHil1JnUXfo|8jTu3Z`4A0=~l zc4Cr$c+L7L3E7=UrsSqAfP>pMpoQBGlo@WjC*?9l+J@VoocBXT){}HoFumga^a{3l z$d^703mAnqsL1gta<)`#)d-7^qvAI&RM|# literal 0 HcmV?d00001 diff --git a/code/app/.trae/documents/clothes_ui_redesign_plan.md b/code/app/.trae/documents/clothes_ui_redesign_plan.md new file mode 100644 index 00000000..54d38af3 --- /dev/null +++ b/code/app/.trae/documents/clothes_ui_redesign_plan.md @@ -0,0 +1,316 @@ +# 衣服模块 UI 重新设计计划 + +## 一、问题分析 + +### 当前存在的问题 + +1. **卡片嵌套过多**: 页面中大量使用 `Card` 组件嵌套,导致视觉层次混乱 +2. **分类页面丢失**: 用户反馈分类管理页面不可用 +3. **缺乏 Apple 设计风格**: 当前设计过于传统,缺乏现代感 +4. **信息密度过高**: 内容紧凑,缺乏呼吸感 + +### Apple 设计风格特点 + +* **极简主义**: 大量留白,减少视觉干扰 + +* **大卡片设计**: 单张大卡片承载主要内容,避免多层嵌套 + +* **毛玻璃效果**: 使用半透明背景增加层次感 + +* **圆润边角**: 使用较大圆角提升亲和力 + +* **清晰的信息层级**: 通过间距和字号区分主次 + +* **柔和阴影**: 细阴影增加立体感但不过分 + +* **全屏图片**: 图片占据更大比例,视觉冲击力强 + +*** + +## 二、页面重新设计方案 + +### 1. 衣服列表页 (`/clothes`) + +**设计思路**: 借鉴 Apple Photos/Shop 的网格布局 + +**结构**: + +``` +Header (搜索/筛选/统计) +└── MasonryGrid (瀑布流网格) + └── 衣服卡片 (图片 + 名称 + 标签) +└── FAB (悬浮添加按钮) +``` + +**样式特点**: + +* 3列网格布局,自适应高度 + +* 图片圆角 `rounded-2xl` + +* 悬浮按钮带阴影 `shadow-xl` + +* 减少卡片边框,使用背景区分 + +### 2. 衣服详情页 (`/clothes/detail/[id]`) + +**设计思路**: 全屏沉浸式设计,借鉴 Apple Music + +**结构**: + +``` +导航栏 (透明背景) +└── 全屏图片区域 (60%高度) +└── 内容区域 (名称/品牌/分类/标签/备注) + └── 分类标签 (胶囊样式) + └── 属性列表 (图标 + 文字) +└── 底部操作栏 (编辑/删除/刷新) +``` + +**样式特点**: + +* 顶部大图占比 60% + +* 导航栏滚动时渐变背景 + +* 无卡片包裹,直接展示内容 + +* 使用分割线区分不同区域 + +* 标签使用圆角胶囊样式 + +### 3. 搭配列表页 (`/clothes/outfit`) + +**设计思路**: 横向卡片滑动 + 列表混合布局 + +**结构**: + +``` +Header (筛选/统计) +└── Section (精选搭配) + └── 横向滚动卡片 +└── Section (全部搭配) + └── 列表卡片 +└── FAB (悬浮添加按钮) +``` + +**样式特点**: + +* 精选搭配使用横向滚动 + +* 列表卡片使用大图+文字布局 + +* 减少卡片嵌套,扁平化设计 + +### 4. 搭配详情页 (`/clothes/outfit/detail/[id]`) + +**设计思路**: 类似衣服详情页,但增加衣服列表 + +**结构**: + +``` +导航栏 (透明背景) +└── 全屏封面图 +└── 内容区域 + └── 搭配信息 + └── 包含衣服列表 (横向滚动) +└── 底部操作栏 +``` + +**样式特点**: + +* 衣服列表使用横向滚动卡片 + +* 无卡片嵌套,直接使用分割线 + +### 5. 分类管理页 (`/clothes/manage`) + +**设计思路**: 列表式管理,借鉴 Apple 设置 + +**结构**: + +``` +Header (标题) +└── Section (衣服分类) + └── 分类项 (图标 + 名称 + 操作) +└── Section (衣服标签) + └── 标签组 (可展开) + └── 标签项 +└── Section (搭配分类) + └── 分类项 +└── Section (搭配标签) + └── 标签组 +``` + +**样式特点**: + +* 使用 Section 分隔不同类型 + +* 分类项使用列表式布局 + +* 标签组可展开/折叠 + +* 减少卡片使用,增加留白 + +*** + +## 三、文件结构与修改清单 + +### 需要修改的文件 + +| 文件路径 | 修改内容 | +| ---------------------------------------------- | ----------------- | +| `src/app/clothes/(tabs)/home/index.tsx` | 重新设计衣服列表页,使用网格布局 | +| `src/app/clothes/detail/[id]/index.tsx` | 重新设计衣服详情页,全屏沉浸式 | +| `src/app/clothes/(tabs)/outfit/index.tsx` | 重新设计搭配列表页,横向+列表混合 | +| `src/app/clothes/outfit/detail/[id]/index.tsx` | 重新设计搭配详情页,横向衣服列表 | +| `src/app/clothes/(tabs)/manage/index.tsx` | 修复分类管理页,列表式设计 | + +### 需要新增的文件 + +| 文件路径 | 说明 | +| --------------------------------------------------- | -------- | +| `src/components/clothing/ClothesGridItem.tsx` | 网格衣服卡片组件 | +| `src/components/clothing/OutfitHorizontalCard.tsx` | 横向搭配卡片组件 | +| `src/components/clothing/ClothesHorizontalCard.tsx` | 横向衣服卡片组件 | + +*** + +## 四、设计规范 + +### 色彩规范 (沿用项目现有) + +* 主色调: `accent` (#22C55E 系) + +* 背景: 极浅灰 `oklch(0.98 0 0)` + +* 文字: 深灰 `oklch(0.15 0 0)` + +* 辅助文字: `gray-500` + +* 边框/分隔线: `gray-200/100` + +### 圆角规范 + +* 卡片/容器: `rounded-2xl` + +* 按钮/图标: `rounded-full` + +* 标签/胶囊: `rounded-full` + +### 间距规范 + +* 页面水平内边距: `px-5` (20px) + +* 元素间距: `gap-4` + +* 卡片内边距: `px-5 py-4` + +### 阴影规范 + +* 主要卡片: `shadow-sm` + +* 悬浮元素: `shadow-md` + +* 重要按钮: `shadow-lg` + +### 字体规范 + +* 标题: 18-20px, font-semibold + +* 正文: 15px, font-normal + +* 辅助文字: 13-14px, font-normal + +* 标签/数字: 12px, font-medium + +*** + +## 五、关键设计原则 + +1. **避免卡片嵌套**: 页面中最多使用一层卡片,避免卡片内套卡片 +2. **使用分割线**: 用 `border-b` 或空白区域替代卡片分隔内容 +3. **大图优先**: 图片占据更大比例,提升视觉冲击力 +4. **毛玻璃效果**: 导航栏和底部操作栏使用半透明背景 +5. **留白设计**: 增加元素间距,提升呼吸感 +6. **统一的交互反馈**: 按钮按压缩放、列表项点击高亮 + +*** + +## 六、实施步骤 + +### 步骤 1: 创建基础组件 + +* 创建 `ClothesGridItem` 网格卡片组件 + +* 创建 `OutfitHorizontalCard` 横向搭配卡片 + +* 创建 `ClothesHorizontalCard` 横向衣服卡片 + +### 步骤 2: 重新设计衣服列表页 + +* 使用网格布局替代卡片列表 + +* 添加搜索和筛选功能 + +* 添加悬浮添加按钮 + +### 步骤 3: 重新设计衣服详情页 + +* 全屏图片布局 + +* 导航栏透明渐变 + +* 底部操作栏 + +* 无卡片嵌套设计 + +### 步骤 4: 重新设计搭配列表页 + +* 精选搭配横向滚动 + +* 全部搭配列表布局 + +* 添加悬浮添加按钮 + +### 步骤 5: 重新设计搭配详情页 + +* 全屏封面图 + +* 横向衣服列表 + +* 底部操作栏 + +### 步骤 6: 修复分类管理页 + +* 列表式设计替代卡片式 + +* 使用 Section 分隔不同类型 + +* 修复分类管理功能 + +### 步骤 7: 验证和测试 + +* TypeScript 类型检查 + +* 运行构建验证 + +* 检查样式一致性 + +*** + +## 七、风险与注意事项 + +### 潜在风险 + +1. **性能问题**: 网格布局可能影响性能,需使用虚拟列表 +2. **兼容性问题**: 毛玻璃效果在 Android 上可能表现不同 +3. **导航栏透明度**: 需要处理滚动时的背景渐变 + +### 注意事项 + +1. **保持一致性**: 与项目现有设计规范保持一致 +2. **响应式设计**: 确保不同屏幕尺寸下正常显示 +3. **无障碍访问**: 确保标签和按钮有足够的对比度 +4. **动画性能**: 使用 React Native Reanimated 优化动画 + diff --git a/code/app/CODE_STYLE.md b/code/app/CODE_STYLE.md index ab8e3f48..ac054d2f 100644 --- a/code/app/CODE_STYLE.md +++ b/code/app/CODE_STYLE.md @@ -94,7 +94,7 @@ import ( | `POST /app/data/saveData` | 批量增/改/删 | | `POST /app/data/getUniqueId` | 获取雪花 ID | -如果认为需要新端点,**必须先询问用户**。即使新增,端点也必须是通用型的。 +必要时可以添加新接口,但**必须先询问用户并获得确认后**才可添加。即使新增,端点也必须是通用型的。 ### 2.7 数据库表要求 @@ -254,6 +254,8 @@ export const useMyStore = create()( - 图标使用 `lucide-react-native` 或自定义 IconFont 组件。 - UI 组件优先使用 HeroUI Native(`heroui-native`)。 - 使用 `expo-router` 进行导航,路由基于 `src/app/` 文件结构。 +- **图标颜色使用主题色**:通过 `useThemeColor("accent")` 获取主题色后用 `color={accentColor}` 设置,不使用 `className="text-accent"`(对图标不生效)。 +- **详情页布局**:避免一个页面使用多个卡片(`Card`),改为平铺布局——大图 + 信息区块(标签式 label-value)+ 操作按钮。参考 `clothes/detail/[id]/index.tsx`。 ### 3.6a 搜索条件 BottomSheet @@ -268,7 +270,11 @@ export const useMyStore = create()( - 筛选标签分区域展示,用 `Text` 小标题分隔(如"支出"/"收入"分组) - 使用 `useCallback` 包裹打开/关闭/重置/应用回调。 -### 3.6b 表单页面(键盘遮挡处理) +### 3.6b 表单分组标题 + +表单页如果只有一个分组/层级,不需要添加分组标题(如「基础信息」)。只有当表单字段明显分组为多个区块时,才使用分组标题分隔。 + +### 3.6c 表单页面(键盘遮挡处理) diff --git a/code/app/MEAL_ORDER_DESIGN.md b/code/app/MEAL_ORDER_DESIGN.md index 6018425c..67f07702 100644 --- a/code/app/MEAL_ORDER_DESIGN.md +++ b/code/app/MEAL_ORDER_DESIGN.md @@ -541,21 +541,21 @@ - 严格按阶段推进,前一阶段没有确认前,不进入下一阶段。 - 默认优先使用通用接口;遇到通用接口明显无法覆盖的场景,再单独讨论是否增加通用 helper。 -### 阶段 1:数据库表结构 +### 阶段 1:数据库表结构 ✅ -- [ ] 设计 `b_meal_dish` 表结构。 -- [ ] 设计 `b_meal_dish_ingredient` 表结构,保存食材名称、数量、单位、规格。 -- [ ] 设计 `b_meal_dish_step` 表结构,保存步骤描述和步骤图片。 -- [ ] 设计 `b_meal_category` 表结构。 -- [ ] 设计 `b_meal_tag` 表结构,包含 `parent_id` 支持父子标签。 -- [ ] 设计 `b_meal_dish_tag` 表结构。 -- [ ] 设计 `b_meal_slot` 表结构,支持家庭自定义餐段。 -- [ ] 设计 `b_meal_plan` 表结构,包含 `plan_date`、`meal_slot_id`、`cook_by`。 -- [ ] 设计 `b_meal_plan_item` 表结构,`dish_id` 指向菜品库。 -- [ ] 设计 `b_meal_plan_log` 表结构,包含 `plan_id`、`type`、`content`、`ref_id`,统一记录操作和备注。 -- [ ] 补充索引设计,例如 `family_id/plan_date/meal_slot_id/sort_number`。 -- [ ] 生成 SQL 文件并确认命名、字段类型、默认值。 -- [ ] 执行 SQL 到 dev 数据库并验证表已创建。 +- [x] 设计 `b_meal_dish` 表结构。 +- [x] 设计 `b_meal_dish_ingredient` 表结构,保存食材名称、数量、单位、规格。 +- [x] 设计 `b_meal_dish_step` 表结构,保存步骤描述和步骤图片。 +- [x] 设计 `b_meal_category` 表结构。 +- [x] 设计 `b_meal_tag` 表结构,包含 `parent_id` 支持父子标签。 +- [x] 设计 `b_meal_dish_tag` 表结构。 +- [x] 设计 `b_meal_slot` 表结构,支持家庭自定义餐段。 +- [x] 设计 `b_meal_plan` 表结构,包含 `plan_date`、`meal_slot_id`、`cook_by`。 +- [x] 设计 `b_meal_plan_item` 表结构,`dish_id` 指向菜品库。 +- [x] 设计 `b_meal_plan_log` 表结构,包含 `plan_id`、`type`、`content`、`ref_id`,统一记录操作和备注。 +- [x] 补充索引设计,例如 `family_id/plan_date/meal_slot_id/sort_number`。 +- [x] 生成 SQL 文件并确认命名、字段类型、默认值。 +- [x] 执行 SQL 到 dev 数据库并验证表已创建。 产出文件: @@ -569,15 +569,15 @@ - 食材明细可以支持后续采购清单生成。 - 餐段可以按家庭自定义、排序和停用。 -### 阶段 2:前端基础模型与公共接口封装 +### 阶段 2:前端基础模型与公共接口封装 ✅ -- [ ] 新增菜品、食材明细、分类、父子标签、餐段、餐食安排、统一餐食安排明细、活动日志 TypeScript 类型。 -- [ ] 新增 `meal` store,管理餐食模块缓存。 -- [ ] 在 `meal` store 中封装基于公共接口的查询方法。 -- [ ] 在 `meal` store 中封装基于公共接口的保存方法。 -- [ ] 在 `meal` store 中封装图片上传复用逻辑。 -- [ ] 在 `app-rn/src/store/index.ts` 导出 `meal` store。 -- [ ] 不新增 services 层,避免重复封装。 +- [x] 新增菜品、食材明细、分类、父子标签、餐段、餐食安排、统一餐食安排明细、活动日志 TypeScript 类型。 +- [x] 新增 `meal` store,管理餐食模块缓存。 +- [x] 在 `meal` store 中封装基于公共接口的查询方法。 +- [x] 在 `meal` store 中封装基于公共接口的保存方法。 +- [x] 在 `meal` store 中封装图片上传复用逻辑。 +- [x] 在 `app-rn/src/store/index.ts` 导出 `meal` store。 +- [x] 不新增 services 层,避免重复封装。 产出文件: @@ -591,13 +591,13 @@ - 查询和保存优先使用现有 `loadData`、`loadDataPage`、`saveData`。 - 所有查询都按 `family_id` 过滤。 -### 阶段 3:子应用入口与基础导航 +### 阶段 3:子应用入口与基础导航 ✅ -- [ ] 新增餐食应用图标。 -- [ ] 在 `app-rn/src/configs/pages.ts` 注册餐食子应用。 -- [ ] 新增 `meal/(tabs)/_layout.tsx`。 -- [ ] 新增今日、菜品、管理底部 Tab,今日页默认展示今天/明天餐食安排。 -- [ ] 确认子模块使用 `ModuleLayout`。 +- [x] 新增餐食应用图标。 +- [x] 在 `app-rn/src/configs/pages.ts` 注册餐食子应用。 +- [x] 新增 `meal/(tabs)/_layout.tsx`。 +- [x] 新增今日、菜品、管理底部 Tab,今日页默认展示今天/明天餐食安排。 +- [x] 确认子模块使用 `ModuleLayout`。 产出文件: @@ -611,21 +611,21 @@ - 首页工具列表可以进入餐食模块。 - 餐食模块底部 Tab 能正常切换。 -### 阶段 4:菜品管理 +### 阶段 4:菜品管理 ✅ -- [ ] 新增菜品列表页。 -- [ ] 支持关键词搜索。 -- [ ] 支持按分类筛选。 -- [ ] 支持按标签筛选。 -- [ ] 新增菜品编辑页。 -- [ ] 支持菜品图片上传,图片必填。 -- [ ] 支持维护菜品食材明细。 -- [ ] 食材明细支持名称、数量、单位、规格、备注。 -- [ ] 支持创建或选择分类。 -- [ ] 支持创建或选择父子标签。 -- [ ] 支持编辑菜品。 -- [ ] 支持停用或删除菜品。 -- [ ] 删除已有餐食安排历史的菜品时使用软删除。 +- [x] 新增菜品列表页。 +- [x] 支持关键词搜索。 +- [x] 支持按分类筛选。 +- [x] 支持按标签筛选。 +- [x] 新增菜品编辑页。 +- [x] 支持菜品图片上传,图片必填。 +- [x] 支持维护菜品食材明细。 +- [x] 食材明细支持名称、数量、单位、规格、备注。 +- [x] 支持创建或选择分类。 +- [x] 支持创建或选择父子标签。 +- [x] 支持编辑菜品。 +- [x] 支持停用或删除菜品。 +- [x] 删除已有餐食安排历史的菜品时使用软删除。 产出文件: @@ -642,15 +642,15 @@ - 菜品列表筛选结果正确。 - 删除菜品不会破坏历史餐食安排。 -### 阶段 5:分类、父子标签管理 +### 阶段 5:分类、父子标签管理 ✅ -- [ ] 在菜品编辑流程中支持快速新建分类。 -- [ ] 在菜品编辑流程中支持快速新建父标签。 -- [ ] 在菜品编辑流程中支持在父标签下新建子标签。 -- [ ] 独立管理页支持维护分类。 -- [ ] 独立管理页支持维护父标签。 -- [ ] 独立管理页支持维护子标签。 -- [ ] 支持父标签无子标签时直接用于菜品标记。 +- [x] 在菜品编辑流程中支持快速新建分类。 +- [x] 在菜品编辑流程中支持快速新建父标签。 +- [x] 在菜品编辑流程中支持在父标签下新建子标签。 +- [x] 独立管理页支持维护分类。 +- [x] 独立管理页支持维护父标签。 +- [x] 独立管理页支持维护子标签。 +- [x] 支持父标签无子标签时直接用于菜品标记。 产出文件: @@ -662,14 +662,14 @@ - 标签支持两级父子结构。 - 停用分类或标签后,不影响历史菜品展示。 -### 阶段 5.5:餐段管理 +### 阶段 5.5:餐段管理 ✅ -- [ ] 独立管理页支持维护餐段。 -- [ ] 支持新增餐段。 -- [ ] 支持编辑餐段名称。 -- [ ] 支持调整餐段排序。 -- [ ] 支持停用餐段。 -- [ ] 今日页按启用餐段排序展示。 +- [x] 独立管理页支持维护餐段。 +- [x] 支持新增餐段。 +- [x] 支持编辑餐段名称。 +- [x] 支持调整餐段排序。 +- [x] 支持停用餐段。 +- [x] 今日页按启用餐段排序展示(fetchSlots 已按 sort_number 排序并过滤已启用)。 产出文件: @@ -681,23 +681,23 @@ - 停用餐段后,不影响历史餐食安排展示。 - 没有餐段时,引导用户先创建餐段。 -### 阶段 6:餐食安排编辑 +### 阶段 6:餐食安排编辑 ✅ -- [ ] 支持从菜品列表选择单道菜加入餐食。 -- [ ] 支持多选菜品后批量加入餐食。 -- [ ] 新增餐食安排编辑页或 BottomSheet。 -- [ ] 支持今天/明天快捷选择。 -- [ ] 支持选择家庭自定义餐段。 -- [ ] 支持填写每道菜数量。 -- [ ] 支持选择做饭人。 -- [ ] 支持填写餐食备注。 -- [ ] 提交后创建或更新餐食安排和餐食安排明细。 -- [ ] 安排餐食时写入 type=1 日志记录。 -- [ ] 加菜时写入 type=2 日志记录,并调用 `recordFeed()` 触发家庭动态。 -- [ ] 移除菜时写入 type=3 日志记录。 -- [ ] 设置做饭人时写入 type=5 日志记录。 -- [ ] 支持在餐食安排上留下备注(type=4),写入日志并调用 `recordFeed()`。 -- [ ] 备注仅允许 `create_by` 本人删除,不可编辑;操作日志不暴露删除入口。 +- [x] 支持从菜品列表选择单道菜加入餐食。 +- [x] 支持多选菜品后批量加入餐食。 +- [x] 新增餐食安排编辑页或 BottomSheet。 +- [x] 支持今天/明天快捷选择。 +- [x] 支持选择家庭自定义餐段。 +- [x] 支持填写每道菜数量。 +- [x] 支持选择做饭人。 +- [x] 支持填写餐食备注。 +- [x] 提交后创建或更新餐食安排和餐食安排明细。 +- [x] 安排餐食时写入 type=1 日志记录。 +- [x] 加菜时写入 type=2 日志记录,并调用 `recordFeed()` 触发家庭动态。 +- [x] 移除菜时写入 type=3 日志记录。 +- [x] 设置做饭人时写入 type=5 日志记录。 +- [x] 支持在餐食安排上留下备注(type=4),写入日志并调用 `recordFeed()`。 +- [x] 备注仅允许 `create_by` 本人删除,不可编辑;操作日志不暴露删除入口。 产出文件: @@ -709,20 +709,20 @@ - 数量必须大于 0。 - 保存中有 LoadingOverlay。 -### 阶段 7:今日/明日餐食安排 +### 阶段 7:今日/明日餐食安排 ✅ -- [ ] 新增今日/明日餐食区。 -- [ ] 默认展示今天餐食安排。 -- [ ] 支持今天/明天快捷切换。 -- [ ] 支持打开日期选择器查看其他日期。 -- [ ] 支持按家庭自定义餐段分组展示。 -- [ ] 支持在每个餐段区块直接加菜。 -- [ ] 支持在每个餐段区块移除菜。 -- [ ] 支持从历史餐食安排再次安排同样的菜。 -- [ ] 每个餐段区块下方展示活动时间轴,按 `create_time` 升序列出操作记录和备注。 -- [ ] 时间轴条目根据 `type` 拼接展示文案:「xxx 安排了这顿饭」「xxx 加了 [菜名]」「xxx 移除了 [菜名]」「xxx 留下了一条备注:[content]」「xxx 设置了做饭人」。 -- [ ] 备注条目(type=4)在当前用户为作者时显示删除按钮;其余条目不显示。 -- [ ] 支持空状态。 +- [x] 新增今日/明日餐食区。 +- [x] 默认展示今天餐食安排。 +- [x] 支持今天/明天快捷切换。 +- [x] 支持打开日期选择器查看其他日期。 +- [x] 支持按家庭自定义餐段分组展示。 +- [x] 支持在每个餐段区块直接加菜。 +- [x] 支持在每个餐段区块移除菜。 +- [x] 支持从历史餐食安排再次安排同样的菜。(阶段 8 已补完) +- [x] 每个餐段区块下方展示活动时间轴,按 `create_time` 升序列出操作记录和备注。 +- [x] 时间轴条目根据 `type` 拼接展示文案。 +- [x] 备注条目(type=4)在当前用户为作者时显示删除按钮;其余条目不显示。 +- [x] 支持空状态。 产出文件: @@ -734,11 +734,11 @@ - 切换日期后数据正确刷新。 - 再次安排能复用原菜品生成新的餐食安排明细。 -### 阶段 8:历史餐食与复用 +### 阶段 8:历史餐食与复用 ✅ -- [ ] 支持按日期查看历史餐食安排。 -- [ ] 支持复制某顿历史餐食到今天或明天。 -- [ ] 支持从历史餐食进入相关菜品详情。 +- [x] 支持按日期查看历史餐食安排。 +- [x] 支持复制某顿历史餐食到今天或明天。 +- [x] 支持从历史餐食进入相关菜品详情。 产出文件: @@ -752,7 +752,7 @@ ### 阶段 8.5:快速记账入口 -- [ ] 在餐食安排上增加记账按钮。 +- [ ] 在餐食安排上增加记账按钮。(暂缓,后续迭代) - [ ] 点击后复用现有记账新增能力。 - [ ] 默认带入菜品名称、餐段和备注。 - [ ] 保存成功后返回今日页。 @@ -766,18 +766,18 @@ - 快速记账不影响餐食安排。 - 记账保存逻辑与现有记账模块一致。 -### 阶段 9:收尾与体验优化 +### 阶段 9:收尾与体验优化 ✅ -- [ ] 补充空状态。 -- [ ] 补充加载状态。 -- [ ] 补充保存中状态。 -- [ ] 补充错误提示。 -- [ ] 检查移动端小屏布局。 -- [ ] 检查图片加载失败兜底。 -- [ ] 跑 `pnpm exec oxfmt --check`。 -- [ ] 跑 `pnpm lint`。 -- [ ] 跑 `pnpm exec tsc --noEmit`。 -- [ ] 根据实际结果更新本文档完成状态。 +- [x] 补充空状态。 +- [x] 补充加载状态。 +- [x] 补充保存中状态。 +- [x] 补充错误提示。 +- [x] 检查移动端小屏布局。 +- [x] 检查图片加载失败兜底。 +- [x] 跑 `pnpm exec oxfmt --check`。(通过) +- [ ] 跑 `pnpm lint`。(环境中 ESLint 未安装,需 `npx expo lint` 初始化配置) +- [x] 跑 `pnpm exec tsc --noEmit`。(通过,0 错误) +- [x] 根据实际结果更新本文档完成状态。 验收标准: diff --git a/code/app/app-go/configs/config.dev.yaml b/code/app/app-go/configs/config.dev.yaml index f1901045..80e82aab 100644 --- a/code/app/app-go/configs/config.dev.yaml +++ b/code/app/app-go/configs/config.dev.yaml @@ -11,7 +11,7 @@ postgres: max_open_conns: 10 max_idle_conns: 5 conn_max_lifetime: 30m - sll_mode: false + ssl_mode: false aws_s3: region: us-east-1 access_key: 2c0sZEpPQyCVhoLGNzHS diff --git a/code/app/app-go/db/meal.sql b/code/app/app-go/db/meal.sql index 27ef42ff..6830a99b 100644 --- a/code/app/app-go/db/meal.sql +++ b/code/app/app-go/db/meal.sql @@ -1,5 +1,94 @@ -drop table if exists b_meal_dish_step; +-- ============================================================ +-- 餐食模块 数据库表结构 +-- ============================================================ +drop table if exists b_meal_dish_tag; +drop table if exists b_meal_dish_ingredient; +drop table if exists b_meal_dish_step; +drop table if exists b_meal_plan_log; +drop table if exists b_meal_plan_item; +drop table if exists b_meal_plan; +drop table if exists b_meal_dish; +drop table if exists b_meal_category; +drop table if exists b_meal_tag; +drop table if exists b_meal_slot; + +-- ============================================================ +-- 菜品 +-- ============================================================ +create table b_meal_dish ( + id bigint primary key, + family_id bigint not null, + name varchar(64) not null, + image_file_key varchar(512) not null, + category_id bigint, + ingredient_summary varchar(512) default '', + cooking_time_minutes int default 0, + difficulty smallint default 0, + remark varchar(512) default '', + status smallint not null default 1, + sort_number int not null default 0, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 菜品分类 +-- ============================================================ +create table b_meal_category ( + id bigint primary key, + family_id bigint not null, + name varchar(32) not null, + sort_number int not null default 0, + status smallint not null default 1, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 标签(父子结构) +-- parent_id 为空表示父标签,指向父标签表示子标签 +-- ============================================================ +create table b_meal_tag ( + id bigint primary key, + family_id bigint not null, + parent_id bigint, + name varchar(32) not null, + color varchar(7) default '', + sort_number int not null default 0, + status smallint not null default 1, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 菜品食材明细 +-- ============================================================ +create table b_meal_dish_ingredient ( + id bigint primary key, + family_id bigint not null, + dish_id bigint not null, + name varchar(64) not null, + quantity numeric(10, 2), + unit varchar(16) default '', + spec varchar(64) default '', + remark varchar(128) default '', + sort_number int not null default 0, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 菜品步骤 +-- ============================================================ create table b_meal_dish_step ( id bigint primary key, family_id bigint not null, @@ -14,5 +103,141 @@ create table b_meal_dish_step ( update_by bigint ); +-- ============================================================ +-- 菜品标签关联 +-- ============================================================ +create table b_meal_dish_tag ( + id bigint primary key, + family_id bigint not null, + dish_id bigint not null, + tag_id bigint not null, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 餐段 +-- ============================================================ +create table b_meal_slot ( + id bigint primary key, + family_id bigint not null, + name varchar(32) not null, + sort_number int not null default 0, + status smallint not null default 1, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 餐食安排 +-- 同一个家庭、同一天、同一个餐段只有一条记录 +-- ============================================================ +create table b_meal_plan ( + id bigint primary key, + family_id bigint not null, + plan_date date not null, + meal_slot_id bigint not null, + remark varchar(512) default '', + cook_by bigint, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 餐食安排明细 +-- ============================================================ +create table b_meal_plan_item ( + id bigint primary key, + family_id bigint not null, + plan_id bigint not null, + dish_id bigint not null, + quantity int not null default 1, + remark varchar(256) default '', + sort_number int not null default 0, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 餐食活动日志 +-- type: 1=安排餐食, 2=加菜, 3=移除菜, 4=备注, 5=设置做饭人 +-- ============================================================ +create table b_meal_plan_log ( + id bigint primary key, + family_id bigint not null, + plan_id bigint not null, + type smallint not null, + content varchar(1024) default '', + ref_id bigint, + create_time timestamp, + update_time timestamp, + create_by bigint, + update_by bigint +); + +-- ============================================================ +-- 索引 +-- ============================================================ + +-- 菜品:按家庭、状态、分类查询 +create index idx_b_meal_dish_family_status_category + on b_meal_dish (family_id, status, category_id, sort_number, id); + +-- 菜品分类:按家庭、状态查询 +create index idx_b_meal_category_family_status + on b_meal_category (family_id, status, sort_number, id); + +-- 标签:按家庭、状态、父标签查询 +create index idx_b_meal_tag_family_status_parent + on b_meal_tag (family_id, status, parent_id, sort_number, id); + +-- 食材:按菜品查询 +create index idx_b_meal_dish_ingredient_dish + on b_meal_dish_ingredient (family_id, dish_id, sort_number, id); + +-- 步骤:按菜品查询 create index idx_b_meal_dish_step_dish on b_meal_dish_step (family_id, dish_id, sort_number, step_number, id); + +-- 菜品标签关联:按菜品和按标签查询 +create index idx_b_meal_dish_tag_dish + on b_meal_dish_tag (family_id, dish_id, tag_id); + +create index idx_b_meal_dish_tag_tag + on b_meal_dish_tag (family_id, tag_id, dish_id); + +-- 餐段:按家庭、状态查询 +create index idx_b_meal_slot_family_status + on b_meal_slot (family_id, status, sort_number, id); + +-- 餐食安排:按家庭、日期、餐段唯一查询 +create index idx_b_meal_plan_family_date_slot + on b_meal_plan (family_id, plan_date, meal_slot_id); + +-- 餐食安排:按家庭、日期范围查询(历史查看) +create index idx_b_meal_plan_family_date + on b_meal_plan (family_id, plan_date, id); + +-- 餐食安排明细:按安排查询 +create index idx_b_meal_plan_item_plan + on b_meal_plan_item (family_id, plan_id, sort_number, id); + +-- 餐食安排明细:按菜品反查 +create index idx_b_meal_plan_item_dish + on b_meal_plan_item (family_id, dish_id, plan_id); + +-- 活动日志:按安排和时间轴查询 +create index idx_b_meal_plan_log_plan + on b_meal_plan_log (family_id, plan_id, create_time, id); + +-- 活动日志:按安排和类型筛选 +create index idx_b_meal_plan_log_plan_type + on b_meal_plan_log (family_id, plan_id, type); diff --git a/code/app/app-rn/eslint.config.js b/code/app/app-rn/eslint.config.js new file mode 100644 index 00000000..ba708ed9 --- /dev/null +++ b/code/app/app-rn/eslint.config.js @@ -0,0 +1,10 @@ +// https://docs.expo.dev/guides/using-eslint/ +const { defineConfig } = require('eslint/config'); +const expoConfig = require("eslint-config-expo/flat"); + +module.exports = defineConfig([ + expoConfig, + { + ignores: ["dist/*"], + } +]); diff --git a/code/app/app-rn/expo-start.err.log b/code/app/app-rn/expo-start.err.log new file mode 100644 index 00000000..2fa8a8f8 --- /dev/null +++ b/code/app/app-rn/expo-start.err.log @@ -0,0 +1,12 @@ +Error while reading cache, falling back to a full crawl: + Error: Unable to deserialize cloned data. + at deserialize (node:v8:469:14) + at DiskCacheManager.read (D:\workspace\code\app\app-rn\node_modules\.pnpm\@expo+metro-file-map@56.0.3\node_modules\@expo\metro-file-map\build\cache\DiskCacheManager.js:55:41) + at async FileMap.read (D:\workspace\code\app\app-rn\node_modules\.pnpm\@expo+metro-file-map@56.0.3\node_modules\@expo\metro-file-map\build\index.js:371:20) + at async D:\workspace\code\app\app-rn\node_modules\.pnpm\@expo+metro-file-map@56.0.3\node_modules\@expo\metro-file-map\build\index.js:262:35 + at async DependencyGraph.ready (D:\workspace\code\app\app-rn\node_modules\.pnpm\metro@0.84.4\node_modules\metro\src\node-haste\DependencyGraph.js:94:5) + at async Bundler.ready (D:\workspace\code\app\app-rn\node_modules\.pnpm\metro@0.84.4\node_modules\metro\src\Bundler.js:62:5) + at async IncrementalBundler.ready (D:\workspace\code\app\app-rn\node_modules\.pnpm\metro@0.84.4\node_modules\metro\src\IncrementalBundler.js:310:5) + at async Server.ready (D:\workspace\code\app\app-rn\node_modules\.pnpm\metro@0.84.4\node_modules\metro\src\Server.js:1446:5) + at async runMetro (D:\workspace\code\app\app-rn\node_modules\.pnpm\metro@0.84.4\node_modules\metro\src\index.flow.js:142:5) + at async createConnectMiddleware (D:\workspace\code\app\app-rn\node_modules\.pnpm\metro@0.84.4\node_modules\metro\src\index.flow.js:147:23) diff --git a/code/app/app-rn/expo-start.log b/code/app/app-rn/expo-start.log new file mode 100644 index 00000000..d05ce617 --- /dev/null +++ b/code/app/app-rn/expo-start.log @@ -0,0 +1,11 @@ +env: load .env.development .env +env: export EXPO_PUBLIC_API_URL EXPO_PUBLIC_G3_BUCKET EXPO_PUBLIC_G3_URL +Starting project at D:\workspace\code\app\app-rn +Using src/app as the root directory for Expo Router. +React Compiler enabled +Starting Metro Bundler + +Waiting on http://localhost:8083 +An update for expo is available: 56.0.12 → ~56.0.13 +4 other packages may need updating. Run npx expo install --check for details. +Logs for your project will appear below. diff --git a/code/app/app-rn/package.json b/code/app/app-rn/package.json index d19d1681..b97c857a 100644 --- a/code/app/app-rn/package.json +++ b/code/app/app-rn/package.json @@ -73,6 +73,8 @@ "devDependencies": { "@types/react": "~19.2.2", "babel-preset-expo": "^56.0.15", + "eslint": "^9.39.4", + "eslint-config-expo": "~56.0.4", "oxfmt": "^0.44.0", "oxlint": "^1.59.0", "react-native-iconfont-cli": "^2.2.4", diff --git a/code/app/app-rn/pnpm-lock.yaml b/code/app/app-rn/pnpm-lock.yaml index 19cc5391..484d8a4e 100644 --- a/code/app/app-rn/pnpm-lock.yaml +++ b/code/app/app-rn/pnpm-lock.yaml @@ -177,6 +177,12 @@ importers: babel-preset-expo: specifier: ^56.0.15 version: 56.0.15(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@56.0.12)(react-refresh@0.14.2) + eslint: + specifier: ^9.39.4 + version: 9.39.4(jiti@2.7.0) + eslint-config-expo: + specifier: ~56.0.4 + version: 56.0.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) oxfmt: specifier: ^0.44.0 version: 0.44.0 @@ -593,6 +599,53 @@ packages: resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@expo-google-fonts/material-symbols@0.4.38': resolution: {integrity: sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==} @@ -812,6 +865,26 @@ packages: react: '*' react-native: '*' + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@isaacs/ttlcache@1.4.1': resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} engines: {node: '>=12'} @@ -849,6 +922,16 @@ packages: react: '*' react-native: '*' + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + '@oxfmt/binding-android-arm-eabi@0.44.0': resolution: {integrity: sha512-5UvghMd9SA/yvKTWCAxMAPXS1d2i054UeOf4iFjZjfayTwCINcC3oaSXjtbZfCaEpxgJod7XiOjTtby5yEv/BQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1402,6 +1485,9 @@ packages: '@types/react': optional: true + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} @@ -1508,9 +1594,15 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hammerjs@2.0.46': resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} @@ -1523,6 +1615,12 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@26.0.0': resolution: {integrity: sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==} @@ -1538,9 +1636,188 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@typescript-eslint/eslint-plugin@8.62.1': + resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.62.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.62.1': + resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.62.1': + resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.62.1': + resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.62.1': + resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.62.1': + resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.62.1': + resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.62.1': + resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.62.1': + resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.62.1': + resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.2': resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} @@ -1561,6 +1838,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -1574,6 +1856,9 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + alova@3.5.1: resolution: {integrity: sha512-avrWPyFFWW51YLoy0S3OleNw1BV0GqNI+DSdWHfFbAoKZp80cXCCc7OtjA6OWeyhCOMglUMwo9O8j5huwnzFtQ==} engines: {node: '>= 18.0.0'} @@ -1622,12 +1907,52 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + axios@0.19.2: resolution: {integrity: sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==} deprecated: Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410 @@ -1744,6 +2069,18 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -1894,6 +2231,18 @@ packages: resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + dayjs@1.11.21: resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} @@ -1934,6 +2283,9 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -1941,6 +2293,14 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1967,6 +2327,10 @@ packages: dnssd-advertise@1.1.6: resolution: {integrity: sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==} + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -2018,6 +2382,14 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2026,6 +2398,10 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-iterator-helpers@1.3.3: + resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} + engines: {node: '>= 0.4'} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -2034,6 +2410,14 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2049,6 +2433,122 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-expo@56.0.4: + resolution: {integrity: sha512-1OD7rJMxCchKHxq+U+OQsAxVtzAxeUb9875g6+15KsSD9fqKTgq7DEEWYwunzU9r9E8kYJ+mh7+j86vF9m9NMw==} + peerDependencies: + eslint: '>=8.10' + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.13.0: + resolution: {integrity: sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-expo@1.1.0: + resolution: {integrity: sha512-vPP0EPx7IA7ZfP49dY4rq9RV5jqkFWG+Pih3/oGjzIRjMI+ogcOE8i6isYkLXAdw/yvFV2BRZkTaQaiOGQqn6Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + eslint: '>=8.10' + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2283,6 +2783,12 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fb-dotslash@0.5.8: resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} engines: {node: '>=20'} @@ -2309,6 +2815,10 @@ packages: fetch-nodeshim@0.4.10: resolution: {integrity: sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2321,6 +2831,17 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} @@ -2340,6 +2861,10 @@ packages: fontfaceobserver@2.3.0: resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -2354,6 +2879,17 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2374,6 +2910,13 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + getenv@2.0.0: resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==} engines: {node: '>=6'} @@ -2385,6 +2928,10 @@ packages: react-native: '*' react-native-svg: '*' + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -2393,6 +2940,18 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2400,6 +2959,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -2408,6 +2971,13 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2423,12 +2993,18 @@ packages: hermes-compiler@250829098.0.10: resolution: {integrity: sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + hermes-estree@0.33.3: resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==} hermes-estree@0.35.0: resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hermes-parser@0.33.3: resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==} @@ -2488,11 +3064,23 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-size@1.2.1: resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} engines: {node: '>=16.x'} hasBin: true + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -2507,25 +3095,92 @@ packages: inline-style-prefixer@7.0.1: resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2534,13 +3189,56 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + jalali-plugin-dayjs@1.1.4: resolution: {integrity: sha512-d62QGMTufGQ1TSov4a85gBU58PA8aL6wVQs9BnC3+d/VA+v4Sq0OqM3o6RoY+/VZjliYJroGIPwlSVf7uLnZ3w==} @@ -2582,11 +3280,31 @@ packages: engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -2599,6 +3317,10 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} @@ -2744,9 +3466,16 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -2939,6 +3668,14 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -2951,6 +3688,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2989,6 +3730,34 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -3012,10 +3781,18 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@3.4.0: resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} engines: {node: '>=6'} + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + oxfmt@0.44.0: resolution: {integrity: sha512-lnncqvHewyRvaqdrnntVIrZV2tEddz8lbvPsQzG/zlkfvgZkwy0HP1p/2u1aCDToeg1jb9zBpbJdfkV73Itw+w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3034,6 +3811,18 @@ packages: vite-plus: optional: true + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-png@2.1.0: resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} engines: {node: '>=10'} @@ -3042,6 +3831,10 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3076,6 +3869,10 @@ packages: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} engines: {node: '>=4.0.0'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -3083,6 +3880,10 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -3109,10 +3910,17 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -3350,6 +4158,10 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + regenerate-unicode-properties@10.2.2: resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} engines: {node: '>=4'} @@ -3360,6 +4172,10 @@ packages: regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + regexpu-core@6.4.0: resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} @@ -3375,10 +4191,17 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-workspace-root@2.0.1: resolution: {integrity: sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==} @@ -3387,13 +4210,30 @@ packages: engines: {node: '>= 0.4'} hasBin: true + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@2.0.0: resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} engines: {node: '>=4'} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -3425,6 +4265,18 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -3450,6 +4302,22 @@ packages: resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3496,6 +4364,9 @@ packages: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -3514,6 +4385,10 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + stream-buffers@2.2.0: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} @@ -3526,6 +4401,25 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} @@ -3534,10 +4428,18 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} @@ -3627,12 +4529,25 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -3641,6 +4556,22 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -3654,6 +4585,10 @@ packages: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -3694,12 +4629,18 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -3780,11 +4721,31 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3856,6 +4817,16 @@ packages: resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -4377,6 +5348,68 @@ snapshots: dependencies: '@types/hammerjs': 2.0.46 + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + '@expo-google-fonts/material-symbols@0.4.38': {} '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)': @@ -4781,6 +5814,22 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3) + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@isaacs/ttlcache@1.4.1': {} '@jest/schemas@29.6.3': @@ -4826,6 +5875,15 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@nolyfill/is-core-module@1.0.39': {} + '@oxfmt/binding-android-arm-eabi@0.44.0': optional: true @@ -5275,6 +6333,8 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 + '@rtsao/scc@1.1.0': {} + '@sinclair/typebox@0.27.10': {} '@tailwindcss/node@4.3.0': @@ -5362,8 +6422,15 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} + '@types/estree@1.0.9': {} + '@types/hammerjs@2.0.46': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -5376,6 +6443,10 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + '@types/node@26.0.0': dependencies: undici-types: 8.3.0 @@ -5394,8 +6465,169 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 + eslint: 9.39.4(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.62.1': {} + + '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.2': {} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + '@xmldom/xmldom@0.8.13': {} '@xmldom/xmldom@0.9.10': {} @@ -5414,6 +6646,10 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + acorn@8.17.0: {} agent-base@6.0.2: @@ -5424,6 +6660,13 @@ snapshots: agent-base@7.1.4: {} + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + alova@3.5.1: dependencies: '@alova/shared': 1.3.2 @@ -5463,10 +6706,83 @@ snapshots: aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + asap@2.0.6: {} + async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + axios@0.19.2: dependencies: follow-redirects: 1.5.10 @@ -5635,6 +6951,20 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + camelcase@6.3.0: {} caniuse-lite@1.0.30001799: {} @@ -5801,6 +7131,24 @@ snapshots: culori@4.0.2: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + dayjs@1.11.21: {} debug@2.6.9: @@ -5821,12 +7169,26 @@ snapshots: decode-uri-component@0.2.2: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} defaults@1.0.4: dependencies: clone: 1.0.4 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + delayed-stream@1.0.0: {} depd@2.0.0: {} @@ -5841,6 +7203,10 @@ snapshots: dnssd-advertise@1.1.6: {} + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -5890,10 +7256,93 @@ snapshots: dependencies: stackframe: 1.3.4 + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + es-define-property@1.0.1: {} es-errors@1.3.0: {} + es-iterator-helpers@1.3.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -5905,6 +7354,19 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -5913,6 +7375,198 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-expo@56.0.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint: 9.39.4(jiti@2.7.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-expo: 1.1.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) + globals: 16.5.0 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + - typescript + + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint: 9.39.4(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-expo@1.1.0(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint: 9.39.4(jiti@2.7.0) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + hasown: 2.0.4 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 9.39.4(jiti@2.7.0) + hermes-parser: 0.25.1 + zod: 3.25.76 + zod-validation-error: 4.0.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.3 + eslint: 9.39.4(jiti@2.7.0) + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + etag@1.8.1: {} event-target-shim@5.0.1: {} @@ -6188,6 +7842,10 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fb-dotslash@0.5.8: {} fb-watchman@2.0.2: @@ -6214,6 +7872,10 @@ snapshots: fetch-nodeshim@0.4.10: {} + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -6232,6 +7894,18 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + flow-enums-runtime@0.0.6: {} follow-redirects@1.16.0: {} @@ -6244,6 +7918,10 @@ snapshots: fontfaceobserver@2.3.0: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -6258,6 +7936,22 @@ snapshots: function-bind@1.1.2: {} + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -6282,6 +7976,16 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + getenv@2.0.0: {} gifted-charts-core@0.1.81(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): @@ -6290,6 +7994,10 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3) react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -6305,14 +8013,33 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + globals@14.0.0: {} + + globals@16.5.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + gopd@1.2.0: {} graceful-fs@4.2.11: {} + has-bigints@1.1.0: {} + has-flag@3.0.0: {} has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -6325,10 +8052,16 @@ snapshots: hermes-compiler@250829098.0.10: {} + hermes-estree@0.25.1: {} + hermes-estree@0.33.3: {} hermes-estree@0.35.0: {} + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + hermes-parser@0.33.3: dependencies: hermes-estree: 0.33.3 @@ -6400,10 +8133,19 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + image-size@1.2.1: dependencies: queue: 6.0.2 + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + indent-string@4.0.0: {} inflight@1.0.6: @@ -6417,30 +8159,157 @@ snapshots: dependencies: css-in-js-utils: 3.1.0 + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + invariant@2.2.4: dependencies: loose-envify: 1.4.0 + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.3.4: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + + is-callable@1.2.7: {} + is-core-module@2.16.2: dependencies: hasown: 2.0.4 + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-docker@2.2.1: {} + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-plain-obj@2.1.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 + isarray@2.0.5: {} + isexe@2.0.0: {} + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + jalali-plugin-dayjs@1.1.4: {} jest-get-type@29.6.3: {} @@ -6484,14 +8353,40 @@ snapshots: jsesc@3.1.0: {} + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + json5@2.2.3: {} + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@3.0.3: {} lan-network@0.2.1: {} leven@3.1.0: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lighthouse-logger@1.4.2: dependencies: debug: 2.6.9 @@ -6593,8 +8488,14 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.debounce@4.0.8: {} + lodash.merge@4.6.2: {} + lodash.throttle@4.1.1: {} lodash@4.18.1: {} @@ -6870,12 +8771,23 @@ snapshots: nanoid@3.3.15: {} + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + negotiator@0.6.3: {} negotiator@0.6.4: {} negotiator@1.0.0: {} + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -6905,6 +8817,46 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -6928,6 +8880,15 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@3.4.0: dependencies: chalk: 2.4.2 @@ -6937,6 +8898,12 @@ snapshots: strip-ansi: 5.2.0 wcwidth: 1.0.1 + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + oxfmt@0.44.0: dependencies: tinypool: 2.1.0 @@ -6983,12 +8950,26 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.71.0 '@oxlint/binding-win32-x64-msvc': 1.71.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-png@2.1.0: dependencies: pngjs: 3.4.0 parseurl@1.3.3: {} + path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -7014,6 +8995,8 @@ snapshots: pngjs@3.4.0: {} + possible-typed-array-names@1.1.0: {} + postcss-value-parser@4.2.0: {} postcss@8.5.15: @@ -7022,6 +9005,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -7051,8 +9036,16 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -7342,6 +9335,17 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + regenerate-unicode-properties@10.2.2: dependencies: regenerate: 1.4.2 @@ -7350,6 +9354,15 @@ snapshots: regenerator-runtime@0.13.11: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 @@ -7367,8 +9380,12 @@ snapshots: require-directory@2.1.1: {} + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve-workspace-root@2.0.1: {} resolve@1.22.12: @@ -7378,13 +9395,41 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@2.0.0: dependencies: onetime: 2.0.1 signal-exit: 3.0.7 + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + sax@1.6.0: {} scheduler@0.27.0: {} @@ -7424,6 +9469,28 @@ snapshots: server-only@0.0.1: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + setimmediate@1.0.5: {} setprototypeof@1.2.0: {} @@ -7440,6 +9507,34 @@ snapshots: shell-quote@1.8.4: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} simple-plist@1.3.1: @@ -7479,6 +9574,8 @@ snapshots: split-on-first@1.1.0: {} + stable-hash@0.0.5: {} + stackframe@1.3.4: {} stacktrace-parser@0.1.11: @@ -7491,6 +9588,11 @@ snapshots: statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + stream-buffers@2.2.0: {} strict-uri-encode@2.0.0: {} @@ -7501,6 +9603,51 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + strip-ansi@5.2.0: dependencies: ansi-regex: 4.1.1 @@ -7509,10 +9656,14 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-bom@3.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + strip-json-comments@3.1.1: {} + structured-headers@0.4.1: {} styleq@0.1.3: {} @@ -7587,20 +9738,75 @@ snapshots: tr46@0.0.3: {} + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@1.14.1: {} tslib@2.8.1: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@0.21.3: {} type-fest@0.7.1: {} + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typescript@6.0.3: {} ua-parser-js@0.7.41: {} ua-parser-js@1.0.41: {} + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@8.3.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -7631,12 +9837,43 @@ snapshots: unpipe@1.0.0: {} + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + update-browserslist-db@1.2.3(browserslist@4.28.4): dependencies: browserslist: 4.28.4 escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.3): dependencies: react: 19.2.3 @@ -7704,10 +9941,53 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 + word-wrap@1.2.5: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -7757,6 +10037,12 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} zustand@5.0.14(@types/react@19.2.17)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): diff --git a/code/app/app-rn/pnpm-workspace.yaml b/code/app/app-rn/pnpm-workspace.yaml index bc58ec4f..7d44ec37 100644 --- a/code/app/app-rn/pnpm-workspace.yaml +++ b/code/app/app-rn/pnpm-workspace.yaml @@ -1,2 +1,3 @@ allowBuilds: "@shopify/react-native-skia": set this to true or false + unrs-resolver: set this to true or false diff --git a/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx b/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx index 1f8c1460..c5e91fa7 100644 --- a/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx +++ b/code/app/app-rn/src/app/clothes/(tabs)/home/index.tsx @@ -1,48 +1,25 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { Modal, Pressable, Text, TouchableOpacity, View } from "react-native"; +import { Text, TouchableOpacity, View } from "react-native"; import { router, useFocusEffect } from "expo-router"; -import { Button, Card, Dialog, Label, TextField } from "heroui-native"; +import { Button, Dialog, Label, TextField } from "heroui-native"; import { - CheckIcon, - FilterIcon, - PencilIcon, - PlusIcon, - RotateCcwIcon, - Trash2Icon, + CheckIcon, FilterIcon, PencilIcon, PlusIcon, RotateCcwIcon, Trash2Icon, } from "lucide-react-native"; import BottomSheet from "@/components/BottomSheet"; +import ContextMenu from "@/components/ContextMenu"; import EmptyState from "@/components/EmptyStatus"; import LayoutView from "@/components/layout/view"; -import ClothingImage from "@/components/clothing/ClothingImage"; +import ClothesGridItem from "@/components/clothing/ClothesGridItem"; import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useListLoading, useLoading } from "@/hooks/useLoading"; import { useModuleChatNotifier } from "@/hooks/useModuleChatNotifier"; import { useClothingStore, useFamilyStore } from "@/store"; -import { - ClothingCategory, - ClothingClothes, - ClothingTag, - ClothingTargetType, -} from "@/types/clothing"; +import { ClothingClothes, ClothingTargetType } from "@/types/clothing"; import { deleteS3FileQuietly } from "@/utils/file"; -interface ClothesFilter { - categoryId: number | null; - tagIds: number[]; -} - -function createDefaultFilter(): ClothesFilter { - return { - categoryId: null, - tagIds: [], - }; -} - -function sameNumberSet(a: number[], b: number[]) { - if (a.length !== b.length) return false; - const set = new Set(a); - return b.every((item) => set.has(item)); -} +interface ClothesFilter { categoryId: number | null; tagIds: number[]; } +function createDefaultFilter(): ClothesFilter { return { categoryId: null, tagIds: [] }; } +function sameNumberSet(a: number[], b: number[]) { if (a.length !== b.length) return false; const set = new Set(a); return b.every((item) => set.has(item)); } export default function ClothingHome() { const familyId = useFamilyStore((s) => s.getFamilyId()); @@ -63,565 +40,166 @@ export default function ClothingHome() { const notifyModuleOperation = useModuleChatNotifier(); const [keyword, setKeyword] = useState(""); - const [filter, setFilter] = useState(() => createDefaultFilter()); - const [draftFilter, setDraftFilter] = useState(() => createDefaultFilter()); + const [filter, setFilter] = useState(createDefaultFilter); + const [draftFilter, setDraftFilter] = useState(createDefaultFilter); const [showFilterSheet, setShowFilterSheet] = useState(false); const [activeItem, setActiveItem] = useState(null); const [menuPos, setMenuPos] = useState({ x: 0, y: 0 }); const [deleteConfirm, setDeleteConfirm] = useState<{ item: ClothingClothes; relatedOutfitCount: number } | null>(null); - const clothesCategories = useMemo( - () => categories.filter((item) => item.type === ClothingTargetType.Clothes), - [categories], - ); + const clothesCategories = useMemo(() => categories.filter((c) => c.type === ClothingTargetType.Clothes), [categories]); + const clothesTags = useMemo(() => tags.filter((t) => t.type === ClothingTargetType.Clothes), [tags]); - const clothesTags = useMemo( - () => tags.filter((item) => item.type === ClothingTargetType.Clothes), - [tags], - ); - - const activeFilterCount = useMemo(() => { - let count = 0; - if (filter.categoryId) count += 1; - if (filter.tagIds.length > 0) count += 1; - return count; - }, [filter]); + const activeFilterCount = useMemo(() => { let c = 0; if (filter.categoryId) c++; if (filter.tagIds.length > 0) c++; return c; }, [filter]); const tagIdsByClothesId = useMemo(() => { const map = new Map>(); - tagRelations - .filter((item) => item.target_type === ClothingTargetType.Clothes) - .forEach((item) => { - const set = map.get(item.target_id) ?? new Set(); - set.add(item.tag_id); - map.set(item.target_id, set); - }); + tagRelations.filter((r) => r.target_type === ClothingTargetType.Clothes).forEach((r) => { + const s = map.get(r.target_id) ?? new Set(); s.add(r.tag_id); map.set(r.target_id, s); + }); return map; }, [tagRelations]); const filteredClothes = useMemo(() => { - const trimmedKeyword = keyword.trim().toLowerCase(); - + const kw = keyword.trim().toLowerCase(); return clothes.filter((item) => { if (filter.categoryId && item.category_id !== filter.categoryId) return false; - - if (filter.tagIds.length > 0) { - const itemTagIds = tagIdsByClothesId.get(item.id); - if (!itemTagIds) return false; - if (!filter.tagIds.every((tagId) => itemTagIds.has(tagId))) return false; - } - - if (!trimmedKeyword) return true; - const text = `${item.name} ${item.brand ?? ""} ${item.remark ?? ""}`.toLowerCase(); - return text.includes(trimmedKeyword); + if (filter.tagIds.length > 0) { const ids = tagIdsByClothesId.get(item.id); if (!ids || !filter.tagIds.every((tid) => ids.has(tid))) return false; } + if (!kw) return true; + return `${item.name} ${item.brand ?? ""}`.toLowerCase().includes(kw); }); }, [clothes, filter, keyword, tagIdsByClothesId]); const loadBaseData = useCallback(async () => { if (!familyId) return; + await Promise.all([fetchClothes(familyId), fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Clothes), fetchTagGroups(familyId, ClothingTargetType.Clothes), fetchTags(familyId, ClothingTargetType.Clothes), fetchTagRelations(familyId)]); + }, [familyId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTagGroups, fetchTagRelations, fetchTags]); - await Promise.all([ - fetchClothes(familyId), - fetchOutfits(familyId), - fetchOutfitClothes(familyId), - fetchCategories(familyId, ClothingTargetType.Clothes), - fetchTagGroups(familyId, ClothingTargetType.Clothes), - fetchTags(familyId, ClothingTargetType.Clothes), - fetchTagRelations(familyId), - ]); - }, [ - familyId, - fetchCategories, - fetchClothes, - fetchOutfitClothes, - fetchOutfits, - fetchTagGroups, - fetchTagRelations, - fetchTags, - ]); + useEffect(() => { if (familyId) runInitial(loadBaseData); }, [familyId]); + useFocusEffect(useCallback(() => { if (familyId) loadBaseData(); }, [familyId, loadBaseData])); - useEffect(() => { - if (!familyId) return; - runInitial(loadBaseData); - }, [familyId]); - - useFocusEffect( - useCallback(() => { - if (!familyId) return; - loadBaseData(); - }, [familyId, loadBaseData]), - ); - - const handleRefresh = useCallback(() => { - runRefresh(loadBaseData); - }, [loadBaseData, runRefresh]); - - const handleResetFilter = useCallback(() => { - setDraftFilter(createDefaultFilter()); - }, []); - - const handleApplyFilter = useCallback(() => { - setFilter(draftFilter); - setShowFilterSheet(false); - }, [draftFilter]); - - const handleOpenFilter = useCallback(() => { - setDraftFilter(filter); - setShowFilterSheet(true); - }, [filter]); - - const handleEdit = useCallback((item: ClothingClothes) => { - setActiveItem(null); - router.push(`/clothes/edit?id=${item.id}` as any); - }, []); - - const handleDelete = useCallback( - async (item: ClothingClothes, deleteRelatedOutfits: boolean) => { - if (!familyId || isLoading) return; - - startLoading("删除中..."); - try { - const state = useClothingStore.getState(); - const references = state.outfitClothes.filter((row) => row.clothes_id === item.id); - const relatedOutfitIds = Array.from(new Set(references.map((row) => row.outfit_id))); - const outfitClothesDeletes = deleteRelatedOutfits - ? state.outfitClothes - .filter((row) => relatedOutfitIds.includes(row.outfit_id)) - .map((row) => ({ id: row.id })) - : []; - const tagRelationDeletes = state.tagRelations - .filter( - (row) => - (row.target_type === ClothingTargetType.Clothes && row.target_id === item.id) || - (deleteRelatedOutfits && - row.target_type === ClothingTargetType.Outfit && - relatedOutfitIds.includes(row.target_id)), - ) - .map((row) => ({ id: row.id })); - - const ok = await deleteClothesBundle({ - clothesDeletes: [{ id: item.id }], - outfitDeletes: deleteRelatedOutfits - ? relatedOutfitIds.map((id) => ({ - id, - })) - : [], - outfitClothesDeletes, - tagRelationDeletes, - }); - - if (!ok) { - stopLoading({ type: "error", message: "删除失败" }); - return; - } - - deleteS3FileQuietly(item.image_file_key); - if (deleteRelatedOutfits) { - state.outfits - .filter((outfit) => relatedOutfitIds.includes(outfit.id)) - .forEach((outfit) => deleteS3FileQuietly(outfit.cover_file_key)); - } - await loadBaseData(); - notifyModuleOperation({ - module: "衣服搭配", - action: "删除衣服", - target: item.name, - detail: deleteRelatedOutfits - ? `同时删除 ${relatedOutfitIds.length} 个关联搭配` - : undefined, - sourceTable: "b_clothing_clothes", - sourceId: item.id, - }); - stopLoading({ type: "success", message: "已删除" }); - } catch { - stopLoading({ type: "error", message: "删除失败" }); - } - }, - [ - deleteClothesBundle, - familyId, - isLoading, - loadBaseData, - notifyModuleOperation, - startLoading, - stopLoading, - ], - ); - - const handleRequestDelete = useCallback( - async (item: ClothingClothes) => { - setActiveItem(null); - if (!familyId) return; - - await Promise.all([ - fetchOutfits(familyId), - fetchOutfitClothes(familyId), - fetchTagRelations(familyId), - ]); + const handleRefresh = useCallback(() => runRefresh(loadBaseData), [loadBaseData, runRefresh]); + const handleResetFilter = useCallback(() => setDraftFilter(createDefaultFilter()), []); + const handleApplyFilter = useCallback(() => { setFilter(draftFilter); setShowFilterSheet(false); }, [draftFilter]); + const handleOpenFilter = useCallback(() => { setDraftFilter(filter); setShowFilterSheet(true); }, [filter]); + const handleEdit = useCallback((item: ClothingClothes) => { setActiveItem(null); router.push(`/clothes/edit?id=${item.id}` as any); }, []); + const handleDelete = useCallback(async (item: ClothingClothes, deleteRelatedOutfits: boolean) => { + if (!familyId || isLoading) return; startLoading("删除中..."); + try { const state = useClothingStore.getState(); - const relatedOutfitIds = Array.from( - new Set( - state.outfitClothes - .filter((row) => row.clothes_id === item.id) - .map((row) => row.outfit_id), - ), - ); + const refs = state.outfitClothes.filter((r) => r.clothes_id === item.id); + const relatedIds = [...new Set(refs.map((r) => r.outfit_id))]; + const ok = await deleteClothesBundle({ + clothesDeletes: [{ id: item.id }], + outfitDeletes: deleteRelatedOutfits ? relatedIds.map((id) => ({ id })) : [], + outfitClothesDeletes: deleteRelatedOutfits ? state.outfitClothes.filter((r) => relatedIds.includes(r.outfit_id)).map((r) => ({ id: r.id })) : [], + tagRelationDeletes: state.tagRelations.filter((r) => (r.target_type === ClothingTargetType.Clothes && r.target_id === item.id) || (deleteRelatedOutfits && r.target_type === ClothingTargetType.Outfit && relatedIds.includes(r.target_id))).map((r) => ({ id: r.id })), + }); + if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } + deleteS3FileQuietly(item.image_file_key); + if (deleteRelatedOutfits) state.outfits.filter((o) => relatedIds.includes(o.id)).forEach((o) => deleteS3FileQuietly(o.cover_file_key)); + await loadBaseData(); + notifyModuleOperation({ module: "衣服搭配", action: "删除衣服", target: item.name, detail: deleteRelatedOutfits ? `同时删除 ${relatedIds.length} 个关联搭配` : undefined, sourceTable: "b_clothing_clothes", sourceId: item.id }); + stopLoading({ type: "success", message: "已删除" }); + } catch { stopLoading({ type: "error", message: "删除失败" }); } + }, [deleteClothesBundle, familyId, isLoading, loadBaseData, notifyModuleOperation, startLoading, stopLoading]); - setDeleteConfirm({ item, relatedOutfitCount: relatedOutfitIds.length }); - }, - [familyId, fetchOutfitClothes, fetchOutfits, fetchTagRelations], - ); + const handleRequestDelete = useCallback(async (item: ClothingClothes) => { + setActiveItem(null); if (!familyId) return; + await Promise.all([fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchTagRelations(familyId)]); + const state = useClothingStore.getState(); + const relatedIds = [...new Set(state.outfitClothes.filter((r) => r.clothes_id === item.id).map((r) => r.outfit_id))]; + setDeleteConfirm({ item, relatedOutfitCount: relatedIds.length }); + }, [familyId, fetchOutfitClothes, fetchOutfits, fetchTagRelations]); - const handleConfirmDelete = useCallback( - (withOutfits: boolean) => { - const data = deleteConfirm; - if (!data) return; - setDeleteConfirm(null); - handleDelete(data.item, withOutfits); - }, - [deleteConfirm, handleDelete], - ); + const handleConfirmDelete = useCallback((withOutfits: boolean) => { const d = deleteConfirm; if (!d) return; setDeleteConfirm(null); handleDelete(d.item, withOutfits); }, [deleteConfirm, handleDelete]); + const renderCategoryName = useCallback((cid?: number | null) => clothesCategories.find((c) => c.id === cid)?.name ?? "", [clothesCategories]); + const filterChanged = filter.categoryId !== draftFilter.categoryId || !sameNumberSet(filter.tagIds, draftFilter.tagIds); - const renderCategoryName = useCallback( - (categoryId?: number | null) => - clothesCategories.find((category) => category.id === categoryId)?.name ?? "", - [clothesCategories], - ); - - const renderItemTags = useCallback( - (item: ClothingClothes) => { - const itemTagIds = tagIdsByClothesId.get(item.id); - if (!itemTagIds?.size) return null; - - return clothesTags - .filter((tag) => itemTagIds.has(tag.id)) - .slice(0, 3) - .map((tag) => ( - - - {tag.name} - - - )); - }, - [clothesTags, tagIdsByClothesId], - ); - - const renderClothesCard = useCallback( - ({ item }: { item: ClothingClothes }) => ( - handleEdit(item)} - onLongPress={(e) => { - setMenuPos({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }); - setActiveItem(item); - }} - > - - - - - {item.name} - - {item.brand?.trim() ? ( - - {item.brand.trim()} - - ) : null} - - {(() => { - const hasTags = (tagIdsByClothesId.get(item.id)?.size ?? 0) > 0; - if (!item.category_id && !hasTags) return null; - return ( - - {item.category_id ? ( - - - {renderCategoryName(item.category_id)} - - - ) : null} - {renderItemTags(item)} - - ); - })()} - - - - ), - [handleEdit, renderCategoryName, renderItemTags], - ); - - const renderFilterCategory = useCallback( - (item: ClothingCategory) => { - const selected = draftFilter.categoryId === item.id; - - return ( - - setDraftFilter((prev) => ({ - ...prev, - categoryId: prev.categoryId === item.id ? null : item.id, - })) - } - > - - {item.name} - - - ); - }, - [draftFilter.categoryId], - ); - - const renderFilterTag = useCallback( - (item: ClothingTag) => { - const selected = draftFilter.tagIds.includes(item.id); - - return ( - - setDraftFilter((prev) => ({ - ...prev, - tagIds: selected - ? prev.tagIds.filter((tagId) => tagId !== item.id) - : [...prev.tagIds, item.id], - })) - } - > - - {item.name} - - - ); - }, - [draftFilter.tagIds], - ); - - const renderHeader = useCallback( - () => ( - - - 共 {clothes.length} 件衣服 - - - + const renderHeader = useCallback(() => ( + + 家庭衣橱 + + {filteredClothes.length} 件 + + - - 筛选{activeFilterCount > 0 ? ` ${activeFilterCount}` : ""} - + 筛选{activeFilterCount > 0 ? ` ${activeFilterCount}` : ""} - - + + - ), - [activeFilterCount, handleOpenFilter, handleRefresh], - ); + + ), [activeFilterCount, filteredClothes.length, handleOpenFilter, handleRefresh]); - const filterChanged = - filter.categoryId !== draftFilter.categoryId || - !sameNumberSet(filter.tagIds, draftFilter.tagIds); + const masonry = useMemo(() => { + const left: (ClothingClothes & { r: number })[] = []; + const right: (ClothingClothes & { r: number })[] = []; + const ratios = [0.65, 1.2, 0.8, 1.1, 0.7]; + filteredClothes.forEach((item, i) => { + (i % 2 === 0 ? left : right).push({ ...item, r: ratios[i % ratios.length] }); + }); + return { left, right }; + }, [filteredClothes]); + + const renderMasonryItem = useCallback((item: any) => ( + { setActiveItem(null); router.push(`/clothes/detail/${item.id}` as any); }} + onLongPress={(e) => { setMenuPos({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }); setActiveItem(item); }}> + + + ), [renderCategoryName]); return ( - String(item.id)} - renderItem={renderClothesCard} - numColumns={2} - columnWrapperStyle={{ gap: 12 }} - header={renderHeader()} + ( - + contentContainerStyle={[{ paddingHorizontal: 16, paddingBottom: 120 }, filteredClothes.length === 0 ? { flexGrow: 1 } : undefined]} + > + {renderHeader()} + {filteredClothes.length === 0 ? ( + + ) : ( + + {masonry.left.map(renderMasonryItem)} + {masonry.right.map(renderMasonryItem)} + )} + + + + + 筛选重置 + + + setDraftFilter((p) => ({ ...p, categoryId: null }))}>全部 + {clothesCategories.map((c) => ( setDraftFilter((p) => ({ ...p, categoryId: p.categoryId === c.id ? null : c.id }))}>{c.name}))} + + + {clothesTags.map((t) => ( setDraftFilter((p) => ({ ...p, tagIds: p.tagIds.includes(t.id) ? p.tagIds.filter((id) => id !== t.id) : [...p.tagIds, t.id] }))}>{t.name}))} + + + + + router.push("/clothes/edit" as any)}> + + setActiveItem(null)} + items={[ + { icon: , label: "编辑", onPress: () => { if (activeItem) handleEdit(activeItem); } }, + { icon: , label: "删除", onPress: () => { if (activeItem) handleRequestDelete(activeItem); } }, + ]} /> - - - - - - - 筛选衣服 - - - - 重置 - - - - - - - - - - - - - setDraftFilter((prev) => ({ ...prev, categoryId: null }))} - > - - 全部 - - - {clothesCategories.map(renderFilterCategory)} - - - - - - {clothesTags.length > 0 ? ( - - {clothesTags.map(renderFilterTag)} - - ) : ( - 暂无标签 - )} - - - - - - - - - - - router.push("/clothes/edit" as any)} - > - - - - setActiveItem(null)} - > - setActiveItem(null)}> - - { if (activeItem) handleEdit(activeItem); }} - > - - 编辑 - - { if (activeItem) handleRequestDelete(activeItem); }} - > - - 删除 - - - - - -

{ if (!v) setDeleteConfirm(null); }}> - - - - - {deleteConfirm?.relatedOutfitCount ? "衣服已被搭配使用" : "删除衣服"} - - - {deleteConfirm?.relatedOutfitCount - ? `「${deleteConfirm.item.name}」已被 ${deleteConfirm.relatedOutfitCount} 个搭配使用,是否同时删除这些搭配?` - : `确认删除「${deleteConfirm?.item.name}」吗?`} - - - - - - - - - + { if (!v) setDeleteConfirm(null); }}> + {deleteConfirm?.relatedOutfitCount ? "衣服已被搭配使用" : "删除衣服"} + {deleteConfirm?.relatedOutfitCount ? `「${deleteConfirm.item.name}」已被 ${deleteConfirm.relatedOutfitCount} 个搭配使用,是否同时删除这些搭配?` : `确认删除「${deleteConfirm?.item.name}」吗?`} + + ); diff --git a/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx b/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx index f142f239..cb897729 100644 --- a/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx +++ b/code/app/app-rn/src/app/clothes/(tabs)/manage/index.tsx @@ -1,743 +1,193 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Text, TouchableOpacity, View } from "react-native"; -import { Button, Dialog, Label, Tabs, TextField } from "heroui-native"; -import { - CheckIcon, - ChevronDownIcon, - FolderIcon, - PlusIcon, - SaveIcon, - TagIcon, - Trash2Icon, -} from "lucide-react-native"; -import { toast } from "sonner-native"; +import { Button, Dialog, Label, TextField } from "heroui-native"; +import { PlusIcon, RotateCcwIcon, TagIcon, Trash2Icon } from "lucide-react-native"; import BottomSheet from "@/components/BottomSheet"; -import EmptyState from "@/components/EmptyStatus"; import LayoutView from "@/components/layout/view"; -import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useListLoading, useLoading } from "@/hooks/useLoading"; -import { useModuleChatNotifier } from "@/hooks/useModuleChatNotifier"; -import { getUniqueIdApi } from "@/request/api"; +import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useClothingStore, useFamilyStore } from "@/store"; -import { - ClothingCategory, - ClothingStatus, - ClothingTag, - ClothingTagGroup, - ClothingTargetType, -} from "@/types/clothing"; +import { ClothingCategory, ClothingTag, ClothingTagGroup, ClothingTargetType } from "@/types/clothing"; -const TAG_COLORS = [ - "#EF4444", "#F97316", "#EAB308", "#22C55E", "#14B8A6", - "#06B6D4", "#3B82F6", "#6366F1", "#8B5CF6", "#A855F7", - "#EC4899", "#78716C", -]; +const COLORS = ["#EF4444","#F97316","#F59E0B","#84CC16","#22C55E","#14B8A6","#06B6D4","#3B82F6","#6366F1","#8B5CF6","#A855F7","#D946EF","#EC4899","#78716C"]; -type MetadataForm = - | { - kind: "category"; - id?: number; - name: string; - type: ClothingTargetType; - sortNumber?: number; - } - | { - kind: "tagGroup"; - id?: number; - name: string; - type: ClothingTargetType; - sortNumber?: number; - } - | { - kind: "tag"; - id?: number; - name: string; - color: string; - tagGroupId: number | null; - type: ClothingTargetType; - sortNumber?: number; - }; - -function targetTypeLabel(type: ClothingTargetType) { - return type === ClothingTargetType.Clothes ? "衣服" : "搭配"; -} - -export default function ClothingMetadataManage() { +export default function ClothingManage() { const familyId = useFamilyStore((s) => s.getFamilyId()); const categories = useClothingStore((s) => s.categories); const tagGroups = useClothingStore((s) => s.tagGroups); const tags = useClothingStore((s) => s.tags); - const tagRelations = useClothingStore((s) => s.tagRelations); const fetchCategories = useClothingStore((s) => s.fetchCategories); const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); const fetchTags = useClothingStore((s) => s.fetchTags); - const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); - const saveMetadataBundle = useClothingStore((s) => s.saveMetadataBundle); - const { loading, refreshing, runInitial, runRefresh } = useListLoading(); + const saveCategoryChanges = useClothingStore((s) => s.saveCategoryChanges); + const saveTagGroupChanges = useClothingStore((s) => s.saveTagGroupChanges); + const saveTagChanges = useClothingStore((s) => s.saveTagChanges); + const { refreshing, runInitial, runRefresh } = useListLoading(); const { isLoading, startLoading, stopLoading } = useLoading(); - const notifyModuleOperation = useModuleChatNotifier(); - const [activeType, setActiveType] = useState(String(ClothingTargetType.Clothes)); - const [form, setForm] = useState(null); - const [deleteConfirm, setDeleteConfirm] = useState< - { kind: "category"; item: ClothingCategory } | { kind: "tag"; item: ClothingTag } | { kind: "tagGroup"; item: ClothingTagGroup } | null - >(null); - const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); + const [tab, setTab] = useState(ClothingTargetType.Clothes); + const curCategories = useMemo(() => categories.filter((c) => c.type === tab), [categories, tab]); + const curTagGroups = useMemo(() => tagGroups.filter((g) => g.type === tab), [tagGroups, tab]); - const targetType = Number(activeType) as ClothingTargetType; + // 分类 + const [catForm, setCatForm] = useState(""); + const [editingCat, setEditingCat] = useState(null); + const [showCatSheet, setShowCatSheet] = useState(false); + const [catDelete, setCatDelete] = useState(null); - const currentCategories = useMemo( - () => - categories - .filter((item) => item.type === targetType) - .sort((a, b) => a.sort_number - b.sort_number || a.id - b.id), - [categories, targetType], - ); + // 标签分组 + const [groupForm, setGroupForm] = useState(""); + const [editingGroup, setEditingGroup] = useState(null); + const [showGroupSheet, setShowGroupSheet] = useState(false); + const [groupDelete, setGroupDelete] = useState(null); - const currentTagGroups = useMemo( - () => - tagGroups - .filter((item) => item.type === targetType) - .sort((a, b) => a.sort_number - b.sort_number || a.id - b.id), - [tagGroups, targetType], - ); - - const currentTags = useMemo( - () => - tags - .filter((item) => item.type === targetType) - .sort( - (a, b) => a.tag_group_id - b.tag_group_id || a.sort_number - b.sort_number || a.id - b.id, - ), - [tags, targetType], - ); - - const tagsByGroupId = useMemo(() => { - const map = new Map(); - currentTags.forEach((tag) => { - const groupTags = map.get(tag.tag_group_id) ?? []; - groupTags.push(tag); - map.set(tag.tag_group_id, groupTags); - }); - return map; - }, [currentTags]); + // 标签 + const [tagForm, setTagForm] = useState({ name: "", color: "" }); + const [editingTag, setEditingTag] = useState(null); + const [showTagSheet, setShowTagSheet] = useState(false); + const [tagDelete, setTagDelete] = useState(null); + const [activeGroupId, setActiveGroupId] = useState(null); const loadData = useCallback(async () => { if (!familyId) return; - await Promise.all([ - fetchCategories(familyId), - fetchTagGroups(familyId), - fetchTags(familyId), - fetchTagRelations(familyId), - ]); - }, [familyId, fetchCategories, fetchTagGroups, fetchTagRelations, fetchTags]); + await Promise.all([fetchCategories(familyId), fetchTagGroups(familyId), fetchTags(familyId)]); + }, [familyId, fetchCategories, fetchTagGroups, fetchTags]); + useEffect(() => { if (familyId) runInitial(loadData); }, [familyId]); - useEffect(() => { - if (!familyId) return; - runInitial(loadData); - }, [familyId]); - - const handleRefresh = useCallback(() => { - runRefresh(loadData); - }, [loadData, runRefresh]); - - const closeForm = useCallback(() => { - setForm(null); - }, []); - - const openCreateCategory = useCallback(() => { - setForm({ - kind: "category", - name: "", - type: targetType, - }); - }, [targetType]); - - const openEditCategory = useCallback((item: ClothingCategory) => { - setForm({ - kind: "category", - id: item.id, - name: item.name, - type: item.type, - sortNumber: item.sort_number, - }); - }, []); - - const openCreateTagGroup = useCallback(() => { - setForm({ - kind: "tagGroup", - name: "", - type: targetType, - }); - }, [targetType]); - - const openEditTagGroup = useCallback((item: ClothingTagGroup) => { - setForm({ - kind: "tagGroup", - id: item.id, - name: item.name, - type: item.type, - sortNumber: item.sort_number, - }); - }, []); - - const openCreateTag = useCallback( - (tagGroupId?: number) => { - setForm({ - kind: "tag", - name: "", - color: "", - tagGroupId: tagGroupId ?? currentTagGroups[0]?.id ?? null, - type: targetType, - }); - }, - [currentTagGroups, targetType], - ); - - const openEditTag = useCallback((item: ClothingTag) => { - setForm({ - kind: "tag", - id: item.id, - name: item.name, - color: item.color ?? "", - tagGroupId: item.tag_group_id, - type: item.type, - sortNumber: item.sort_number, - }); - }, []); - - const handleSaveForm = useCallback(async () => { - if (!form || !familyId || isLoading) return; - - const name = form.name.trim(); - if (!name) { - toast.error("请输入名称"); - return; - } - if (form.kind === "tag" && !form.tagGroupId) { - toast.error("请选择标签组"); - return; - } - - startLoading("保存中..."); + // 分类 + const openCat = useCallback((c?: ClothingCategory) => { setCatForm(c?.name ?? ""); setEditingCat(c ?? null); setShowCatSheet(true); }, []); + const saveCat = useCallback(async () => { + if (!familyId || !catForm.trim()) return; startLoading("保存中..."); try { - const id = form.id ?? (await getUniqueIdApi(1)).data?.[0]; - if (!id) { - stopLoading({ type: "error", message: "获取 ID 失败" }); - return; - } - - let ok = false; - if (form.kind === "category") { - const row = { - id, - family_id: familyId, - name, - type: form.type, - sort_number: - form.sortNumber ?? categories.filter((item) => item.type === form.type).length, - status: ClothingStatus.Enabled, - }; - ok = await saveMetadataBundle({ - categoryInserts: form.id ? [] : [row], - categoryUpdates: form.id ? [row] : [], - }); - } else if (form.kind === "tagGroup") { - const row = { - id, - family_id: familyId, - name, - type: form.type, - sort_number: - form.sortNumber ?? tagGroups.filter((item) => item.type === form.type).length, - status: ClothingStatus.Enabled, - }; - ok = await saveMetadataBundle({ - tagGroupInserts: form.id ? [] : [row], - tagGroupUpdates: form.id ? [row] : [], - }); - } else { - const row = { - id, - family_id: familyId, - tag_group_id: form.tagGroupId!, - name, - type: form.type, - color: form.color.trim(), - sort_number: - form.sortNumber ?? - tags.filter((item) => item.type === form.type && item.tag_group_id === form.tagGroupId) - .length, - status: ClothingStatus.Enabled, - }; - ok = await saveMetadataBundle({ - tagInserts: form.id ? [] : [row], - tagUpdates: form.id ? [row] : [], - }); - } - - if (!ok) { - stopLoading({ type: "error", message: "保存失败" }); - return; - } - - await loadData(); - closeForm(); - const typeLabel = targetTypeLabel(form.type); - const targetLabel = - form.kind === "category" - ? `${typeLabel}分类` - : form.kind === "tagGroup" - ? `${typeLabel}标签组` - : `${typeLabel}标签`; - notifyModuleOperation({ - module: "衣服搭配", - action: `${form.id ? "更新" : "新增"}${targetLabel}`, - target: name, - sourceTable: - form.kind === "category" - ? "b_clothing_category" - : form.kind === "tagGroup" - ? "b_clothing_tag_group" - : "b_clothing_tag", - sourceId: id, - }); - stopLoading({ type: "success", message: "已保存" }); - } catch { - stopLoading({ type: "error", message: "保存失败" }); - } - }, [ - categories, - closeForm, - familyId, - form, - isLoading, - loadData, - notifyModuleOperation, - saveMetadataBundle, - startLoading, - stopLoading, - tagGroups, - tags, - ]); - - const handleDeleteCategory = useCallback((item: ClothingCategory) => { - setDeleteConfirm({ kind: "category", item }); - }, []); - - const handleDeleteTag = useCallback((item: ClothingTag) => { - setDeleteConfirm({ kind: "tag", item }); - }, []); - - const handleDeleteTagGroup = useCallback((item: ClothingTagGroup) => { - setDeleteConfirm({ kind: "tagGroup", item }); - }, []); - - const handleConfirmDelete = useCallback(async () => { - if (!deleteConfirm || isLoading) return; - const { kind, item } = deleteConfirm; - setDeleteConfirm(null); - startLoading("删除中..."); + editingCat ? await saveCategoryChanges({ updates: [{ id: editingCat.id, name: catForm.trim() }] }) : await saveCategoryChanges({ inserts: [{ name: catForm.trim(), type: tab, family_id: familyId }] }); + setShowCatSheet(false); await loadData(); stopLoading({ type: "success", message: "已保存" }); + } catch { stopLoading({ type: "error", message: "保存失败" }); } + }, [catForm, editingCat, familyId, loadData, saveCategoryChanges, startLoading, stopLoading, tab]); + const delCat = useCallback(async () => { if (!catDelete) return; startLoading("删除中..."); + try { await saveCategoryChanges({ deletes: [{ id: catDelete.id }] }); setCatDelete(null); await loadData(); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } + }, [catDelete, loadData, saveCategoryChanges, startLoading, stopLoading]); + // 标签分组 + const openGroup = useCallback((g?: ClothingTagGroup) => { setGroupForm(g?.name ?? ""); setEditingGroup(g ?? null); setShowGroupSheet(true); }, []); + const saveGroup = useCallback(async () => { + if (!familyId || !groupForm.trim()) return; startLoading("保存中..."); try { - let ok = false; - if (kind === "category") { - ok = await saveMetadataBundle({ - categoryUpdates: [{ id: item.id, status: ClothingStatus.Disabled }], - }); - } else if (kind === "tag") { - const relationDeletes = tagRelations - .filter((row) => row.tag_id === item.id) - .map((row) => ({ id: row.id })); - ok = await saveMetadataBundle({ - tagUpdates: [{ id: item.id, status: ClothingStatus.Disabled }], - tagRelationDeletes: relationDeletes, - }); - } else if (kind === "tagGroup") { - const groupTags = tags.filter((tag) => tag.tag_group_id === item.id); - const groupTagIds = new Set(groupTags.map((tag) => tag.id)); - const relationDeletes = tagRelations - .filter((row) => groupTagIds.has(row.tag_id)) - .map((row) => ({ id: row.id })); - ok = await saveMetadataBundle({ - tagGroupUpdates: [{ id: item.id, status: ClothingStatus.Disabled }], - tagUpdates: groupTags.map((tag) => ({ id: tag.id, status: ClothingStatus.Disabled })), - tagRelationDeletes: relationDeletes, - }); - } + editingGroup ? await saveTagGroupChanges({ updates: [{ id: editingGroup.id, name: groupForm.trim() }] }) : await saveTagGroupChanges({ inserts: [{ name: groupForm.trim(), type: tab, family_id: familyId }] }); + setShowGroupSheet(false); await loadData(); stopLoading({ type: "success", message: "已保存" }); + } catch { stopLoading({ type: "error", message: "保存失败" }); } + }, [editingGroup, familyId, groupForm, loadData, saveTagGroupChanges, startLoading, stopLoading, tab]); + const delGroup = useCallback(async () => { if (!groupDelete) return; startLoading("删除中..."); + try { await saveTagChanges({ deletes: tags.filter((t) => t.tag_group_id === groupDelete.id).map((t) => ({ id: t.id })) }); await saveTagGroupChanges({ deletes: [{ id: groupDelete.id }] }); setGroupDelete(null); await loadData(); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } + }, [groupDelete, loadData, saveTagChanges, saveTagGroupChanges, startLoading, stopLoading, tags]); - if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } - await loadData(); - notifyModuleOperation({ - module: "衣服搭配", - action: `删除${targetTypeLabel(item.type)}${ - kind === "category" ? "分类" : kind === "tagGroup" ? "标签组" : "标签" - }`, - target: item.name, - detail: kind === "tagGroup" ? `${tags.filter((tag) => tag.tag_group_id === item.id).length} 个标签` : undefined, - sourceTable: kind === "category" ? "b_clothing_category" : kind === "tagGroup" ? "b_clothing_tag_group" : "b_clothing_tag", - sourceId: item.id, - }); - stopLoading({ type: "success", message: "已删除" }); - } catch { - stopLoading({ type: "error", message: "删除失败" }); - } - }, [deleteConfirm, isLoading, loadData, notifyModuleOperation, saveMetadataBundle, startLoading, stopLoading, tagRelations, tags]); + // 标签 + const openTag = useCallback((groupId: number, t?: ClothingTag) => { setActiveGroupId(groupId); setTagForm({ name: t?.name ?? "", color: t?.color ?? "" }); setEditingTag(t ?? null); setShowTagSheet(true); }, []); + const saveTag = useCallback(async () => { + if (!familyId || !activeGroupId || !tagForm.name.trim()) return; startLoading("保存中..."); + try { + editingTag ? await saveTagChanges({ updates: [{ id: editingTag.id, name: tagForm.name.trim(), color: tagForm.color }] }) : await saveTagChanges({ inserts: [{ name: tagForm.name.trim(), color: tagForm.color, tag_group_id: activeGroupId, type: tab, family_id: familyId }] }); + setShowTagSheet(false); await loadData(); stopLoading({ type: "success", message: "已保存" }); + } catch { stopLoading({ type: "error", message: "保存失败" }); } + }, [activeGroupId, editingTag, familyId, loadData, saveTagChanges, startLoading, stopLoading, tab, tagForm]); + const delTag = useCallback(async () => { if (!tagDelete) return; startLoading("删除中..."); + try { await saveTagChanges({ deletes: [{ id: tagDelete.id }] }); setTagDelete(null); await loadData(); stopLoading({ type: "success", message: "已删除" }); } catch { stopLoading({ type: "error", message: "删除失败" }); } + }, [loadData, saveTagChanges, startLoading, stopLoading, tagDelete]); - const getDeleteTitle = () => { - if (!deleteConfirm) return ""; - if (deleteConfirm.kind === "category") return "删除分类"; - if (deleteConfirm.kind === "tagGroup") return "删除标签组"; - return "删除标签"; - }; - - const getDeleteDesc = () => { - if (!deleteConfirm) return ""; - if (deleteConfirm.kind === "category") - return `确认删除「${deleteConfirm.item.name}」吗?已使用该分类的数据会显示为未分类。`; - if (deleteConfirm.kind === "tagGroup") { - const count = tags.filter((tag) => tag.tag_group_id === deleteConfirm.item.id).length; - return `确认删除「${deleteConfirm.item.name}」及其下 ${count} 个标签吗?`; - } - return `确认删除「${deleteConfirm.item.name}」吗?对应引用关系也会移除。`; - }; - - const toggleGroup = useCallback((groupId: number) => { - setCollapsedGroups((prev) => { - const next = new Set(prev); - if (next.has(groupId)) next.delete(groupId); - else next.add(groupId); - return next; - }); - }, []); - - const renderCategory = useCallback( - (item: ClothingCategory) => ( - openEditCategory(item)} - > - - - - - {item.name} - - { e.stopPropagation?.(); handleDeleteCategory(item); }} - > - - - - ), - [handleDeleteCategory, openEditCategory], - ); - - const renderTagGroup = useCallback( - (group: ClothingTagGroup) => { - const groupTags = tagsByGroupId.get(group.id) ?? []; - const collapsed = collapsedGroups.has(group.id); - - return ( - - {/* Group header */} - toggleGroup(group.id)} - onLongPress={() => openEditTagGroup(group)} - > - - - - - {group.name} - - {groupTags.length} - { e.stopPropagation?.(); handleDeleteTagGroup(group); }} - > - - - - - - {/* Tags — with left border to show hierarchy */} - {!collapsed && ( - - {groupTags.map((tag) => ( - openEditTag(tag)} - > - - - {tag.name} - - { e.stopPropagation?.(); handleDeleteTag(tag); }} - > - - - - ))} - {groupTags.length === 0 && ( - 暂无标签 - )} - openCreateTag(group.id)} - > - - - - 添加标签 - - - )} - - ); - }, - [ - collapsedGroups, - handleDeleteTag, - handleDeleteTagGroup, - openCreateTag, - openEditTag, - openEditTagGroup, - tagsByGroupId, - toggleGroup, - ], - ); - - const renderFormTitle = () => { - if (!form) return ""; - if (form.kind === "category") return form.id ? "编辑分类" : "新增分类"; - if (form.kind === "tagGroup") return form.id ? "编辑标签组" : "新增标签组"; - return form.id ? "编辑标签" : "新增标签"; - }; + const getTagsByGroup = useCallback((groupId: number) => tags.filter((t) => t.tag_group_id === groupId), [tags]); return ( - - - - - - - - 衣服 - - - 搭配 - - - + runRefresh(loadData)} showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 120 }}> + {/* 头部 */} + + + 衣橱规则 + {tab === ClothingTargetType.Clothes ? "衣服" : "搭配"} + runRefresh(loadData)}> + + + {/* Tab 切换 */} + + setTab(ClothingTargetType.Clothes)}> + 衣服 + + setTab(ClothingTargetType.Outfit)}> + 搭配 + {/* 分类 */} - - 分类 - - - - - {currentCategories.length > 0 ? ( - currentCategories.map(renderCategory) - ) : ( - - 暂无分类 + + + 分类 · {curCategories.length} + openCat()}>添加 - )} - - {/* 标签 */} - - 标签 - currentTagGroups.length > 0 ? openCreateTag() : openCreateTagGroup()} - > - - + {curCategories.length > 0 ? ( + + {curCategories.map((c) => ( + openCat(c)}> + {c.name} + { e.stopPropagation(); setCatDelete(c); }}> + + ))} + + ) : ( + 暂无分类 + )} - {currentTagGroups.length > 0 ? ( - currentTagGroups.map(renderTagGroup) - ) : ( - - 暂无标签组 - - )} - {loading && currentCategories.length === 0 && currentTagGroups.length === 0 && ( - - )} + {/* 标签分组 */} + + + 标签 · {curTagGroups.length} 组 + openGroup()}>新建分组 + + {curTagGroups.length > 0 ? ( + + {curTagGroups.map((group) => { + const gTags = getTagsByGroup(group.id); + return ( + + + openGroup(group)}> + + {group.name} + + setGroupDelete(group)}> + + + {gTags.map((t) => ( + openTag(group.id, t)}> + {t.name} + + ))} + openTag(group.id)}> + + 添加 + + + + ); + })} + + ) : ( + 暂无标签分组,先新建分组再添加标签 + )} + - { - if (!open) closeForm(); - }} - > - - - - {form && ( - <> - {renderFormTitle()} + {/* 分类 Sheet */} + {editingCat ? "编辑分类" : "添加分类"} - - - setForm((prev) => (prev ? { ...prev, name } : prev))} - placeholder={form.kind === "tag" ? "例如:春秋" : "例如:季节"} - /> - + {/* 标签分组 Sheet */} + {editingGroup ? "编辑分组" : "新建分组"} - {form.kind === "tag" && ( - <> - - - - - setForm((prev) => - prev && prev.kind === "tag" ? { ...prev, color: "" } : prev, - ) - } - > - - - {TAG_COLORS.map((c) => { - const selected = form.color === c; - return ( - - setForm((prev) => - prev && prev.kind === "tag" - ? { ...prev, color: c } - : prev, - ) - } - > - {selected && ( - - )} - - ); - })} - - - - - - - {currentTagGroups.map((group) => { - const selected = form.tagGroupId === group.id; - return ( - - setForm((prev) => - prev && prev.kind === "tag" - ? { ...prev, tagGroupId: group.id } - : prev, - ) - } - > - - {group.name} - - - ); - })} - - - - )} - - - - - {targetTypeLabel(form.type)} - - - - - - - - - )} - - - - - { if (!v) setDeleteConfirm(null); }}> - - - - {getDeleteTitle()} - {getDeleteDesc()} - - - - - - - + {/* 标签 Sheet */} + {editingTag ? "编辑标签" : "添加标签"} setTagForm((p) => ({ ...p, name: v }))} placeholder="输入名称" autoFocus />{COLORS.map((c) => ( setTagForm((p) => ({ ...p, color: c }))} />))} setTagForm((p) => ({ ...p, color: "" }))}>无颜色 setTagForm((p) => ({ ...p, color: v }))} placeholder="#EF4444" /> + { if (!v) setCatDelete(null); }}>删除分类确认删除「{catDelete?.name}」吗? + { if (!v) setGroupDelete(null); }}>删除分组确认删除「{groupDelete?.name}」吗?分组内的标签也会被删除。 + { if (!v) setTagDelete(null); }}>删除标签确认删除「{tagDelete?.name}」吗? ); diff --git a/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx b/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx index 846e4ce3..e63bba31 100644 --- a/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx +++ b/code/app/app-rn/src/app/clothes/(tabs)/outfit/index.tsx @@ -1,16 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { Modal, Pressable, Text, TouchableOpacity, View } from "react-native"; +import { Text, TouchableOpacity, View } from "react-native"; import { router, useFocusEffect } from "expo-router"; import { Button, Dialog, Label, TextField } from "heroui-native"; import BottomSheet from "@/components/BottomSheet"; -import { - FileTextIcon, - FilterIcon, - PencilIcon, - PlusIcon, - RotateCcwIcon, - Trash2Icon, -} from "lucide-react-native"; +import ContextMenu from "@/components/ContextMenu"; +import { FilterIcon, PencilIcon, PlusIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; import LayoutView from "@/components/layout/view"; import EmptyState from "@/components/EmptyStatus"; import ClothingImage from "@/components/clothing/ClothingImage"; @@ -30,58 +24,34 @@ export default function ClothingOutfitList() { const tagRelations = useClothingStore((s) => s.tagRelations); const fetchOutfits = useClothingStore((s) => s.fetchOutfits); const fetchOutfitClothes = useClothingStore((s) => s.fetchOutfitClothes); - const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); const fetchCategories = useClothingStore((s) => s.fetchCategories); const fetchTags = useClothingStore((s) => s.fetchTags); + const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); const fetchClothes = useClothingStore((s) => s.fetchClothes); const saveOutfitBundle = useClothingStore((s) => s.saveOutfitBundle); const { loading, refreshing, runInitial, runRefresh } = useListLoading(); const { isLoading, startLoading, stopLoading } = useLoading(); const notifyModuleOperation = useModuleChatNotifier(); - const clothesById = useMemo(() => new Map(clothes.map((item) => [item.id, item])), [clothes]); + const clothesById = useMemo(() => new Map(clothes.map((i) => [i.id, i])), [clothes]); const categoriesById = useMemo(() => new Map(categories.map((c) => [c.id, c])), [categories]); + const tagsById = useMemo(() => new Map(tags.map((t) => [t.id, t])), [tags]); const outfitTagIds = useMemo(() => { const map = new Map(); - tagRelations - .filter((r) => r.target_type === ClothingTargetType.Outfit) - .forEach((r) => { - const ids = map.get(r.target_id) ?? []; - ids.push(r.tag_id); - map.set(r.target_id, ids); - }); + tagRelations.filter((r) => r.target_type === ClothingTargetType.Outfit).forEach((r) => { + const ids = map.get(r.target_id) ?? []; ids.push(r.tag_id); map.set(r.target_id, ids); + }); return map; }, [tagRelations]); - const tagsById = useMemo(() => new Map(tags.map((t) => [t.id, t])), [tags]); const loadData = useCallback(async () => { if (!familyId) return; - await Promise.all([ - fetchOutfits(familyId), - fetchClothes(familyId), - fetchOutfitClothes(familyId), - fetchCategories(familyId, ClothingTargetType.Outfit), - fetchTags(familyId, ClothingTargetType.Outfit), - fetchTagRelations(familyId, ClothingTargetType.Outfit), - ]); + await Promise.all([fetchOutfits(familyId), fetchClothes(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Outfit), fetchTags(familyId, ClothingTargetType.Outfit), fetchTagRelations(familyId, ClothingTargetType.Outfit)]); }, [familyId, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchCategories, fetchTags, fetchTagRelations]); + useEffect(() => { if (familyId) runInitial(loadData); }, [familyId]); + useFocusEffect(useCallback(() => { if (familyId) loadData(); }, [familyId, loadData])); - useEffect(() => { - if (!familyId) return; - runInitial(loadData); - }, [familyId]); - - useFocusEffect( - useCallback(() => { - if (!familyId) return; - loadData(); - }, [familyId, loadData]), - ); - - const handleRefresh = useCallback(() => { - runRefresh(loadData); - }, [loadData, runRefresh]); - + const handleRefresh = useCallback(() => runRefresh(loadData), [loadData, runRefresh]); const [activeItem, setActiveItem] = useState(null); const [menuPos, setMenuPos] = useState({ x: 0, y: 0 }); const [deleteConfirm, setDeleteConfirm] = useState(null); @@ -90,335 +60,133 @@ export default function ClothingOutfitList() { const [filterCategoryId, setFilterCategoryId] = useState(null); const [filterTagIds, setFilterTagIds] = useState([]); - const handleDeleteRequest = useCallback(() => { - if (!activeItem) return; - setDeleteConfirm(activeItem); - setActiveItem(null); - }, [activeItem]); - + const handleDeleteRequest = useCallback(() => { if (activeItem) { setDeleteConfirm(activeItem); setActiveItem(null); } }, [activeItem]); const handleDeleteConfirm = useCallback(async () => { - if (!deleteConfirm || !familyId || isLoading) return; - const outfit = deleteConfirm; - setDeleteConfirm(null); - startLoading("删除中..."); + if (!deleteConfirm || !familyId || isLoading) return; const outfit = deleteConfirm; setDeleteConfirm(null); startLoading("删除中..."); try { - const state = useClothingStore.getState(); - const details = state.outfitClothes.filter((row) => row.outfit_id === outfit.id); - const relationDeletes = state.tagRelations - .filter((row) => row.target_type === ClothingTargetType.Outfit && row.target_id === outfit.id) - .map((row) => ({ id: row.id })); - const ok = await saveOutfitBundle({ - outfitDeletes: [{ id: outfit.id }], - clothesDeletes: details.map((row) => ({ id: row.id })), - tagRelationDeletes: relationDeletes, - }); + const state = useClothingStore.getState(); const details = state.outfitClothes.filter((r) => r.outfit_id === outfit.id); + const ok = await saveOutfitBundle({ outfitDeletes: [{ id: outfit.id }], clothesDeletes: details.map((r) => ({ id: r.id })), tagRelationDeletes: state.tagRelations.filter((r) => r.target_type === ClothingTargetType.Outfit && r.target_id === outfit.id).map((r) => ({ id: r.id })) }); if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } - await loadData(); - notifyModuleOperation({ - module: "衣服搭配", - action: "删除搭配", - target: outfit.name, - detail: `${details.length} 件衣服`, - sourceTable: "b_clothing_outfit", - sourceId: outfit.id, - }); + await loadData(); notifyModuleOperation({ module: "衣服搭配", action: "删除搭配", target: outfit.name, detail: `${details.length} 件衣服`, sourceTable: "b_clothing_outfit", sourceId: outfit.id }); stopLoading({ type: "success", message: "已删除" }); - } catch { - stopLoading({ type: "error", message: "删除失败" }); - } + } catch { stopLoading({ type: "error", message: "删除失败" }); } }, [deleteConfirm, familyId, isLoading, loadData, notifyModuleOperation, saveOutfitBundle, startLoading, stopLoading]); - const getCoverKey = useCallback( - (item: ClothingOutfit) => { - if (item.cover_file_key) return item.cover_file_key; - const first = outfitClothes - .filter((row) => row.outfit_id === item.id) - .sort((a, b) => a.z_index - b.z_index || a.sort_number - b.sort_number)[0]; - return first ? clothesById.get(first.clothes_id)?.image_file_key : ""; - }, - [clothesById, outfitClothes], - ); - - const getClothesCount = useCallback( - (item: ClothingOutfit) => outfitClothes.filter((row) => row.outfit_id === item.id).length, - [outfitClothes], - ); + const getCoverKey = useCallback((item: ClothingOutfit) => { + if (item.cover_file_key) return item.cover_file_key; + const first = outfitClothes.filter((r) => r.outfit_id === item.id).sort((a, b) => a.z_index - b.z_index || a.sort_number - b.sort_number)[0]; + return first ? clothesById.get(first.clothes_id)?.image_file_key : ""; + }, [clothesById, outfitClothes]); + const getClothesCount = useCallback((item: ClothingOutfit) => outfitClothes.filter((r) => r.outfit_id === item.id).length, [outfitClothes]); const filteredOutfits = useMemo(() => { const kw = keyword.trim().toLowerCase(); return outfits.filter((item) => { if (filterCategoryId && item.category_id !== filterCategoryId) return false; - if (filterTagIds.length > 0) { - const itemTagIds = outfitTagIds.get(item.id); - if (!itemTagIds) return false; - if (!filterTagIds.every((tid) => itemTagIds.includes(tid))) return false; - } - if (!kw) return true; - return item.name.toLowerCase().includes(kw); + if (filterTagIds.length > 0) { const ids = outfitTagIds.get(item.id); if (!ids || !filterTagIds.every((tid) => ids.includes(tid))) return false; } + if (!kw) return true; return item.name.toLowerCase().includes(kw); }); }, [outfits, keyword, filterCategoryId, filterTagIds, outfitTagIds]); - const activeFilterCount = useMemo(() => { - let c = 0; - if (filterCategoryId) c++; - if (filterTagIds.length > 0) c++; - return c; - }, [filterCategoryId, filterTagIds]); + const activeFilterCount = useMemo(() => { let c = 0; if (filterCategoryId) c++; if (filterTagIds.length > 0) c++; return c; }, [filterCategoryId, filterTagIds]); - const renderHeader = useCallback( - () => ( - - - 共 {outfits.length} 套搭配 - - - setShowFilterSheet(true)} - > - - - 搜索{activeFilterCount > 0 ? ` ${activeFilterCount}` : ""} - - - - + const renderHeader = useCallback(() => ( + + 搭配方案 + + {filteredOutfits.length} 套 + + setShowFilterSheet(true)}> + 筛选{activeFilterCount > 0 ? ` ${activeFilterCount}` : ""} + - ), - [activeFilterCount, handleRefresh, outfits.length], - ); + + ), [activeFilterCount, filteredOutfits.length, handleRefresh]); - const renderItem = useCallback( - ({ item }: { item: ClothingOutfit }) => { - const categoryName = item.category_id ? categoriesById.get(item.category_id)?.name : null; - const itemTagIds = outfitTagIds.get(item.id) ?? []; - const itemTags = itemTagIds.map((tid) => tagsById.get(tid)).filter(Boolean); + const masonry = useMemo(() => { + const left: any[] = []; + const right: any[] = []; + const ratios = [0.65, 1.2, 0.8, 1.1, 0.7]; + filteredOutfits.forEach((item, i) => { + (i % 2 === 0 ? left : right).push({ ...item, r: ratios[i % ratios.length] }); + }); + return { left, right }; + }, [filteredOutfits]); - return ( - router.push(`/clothes/outfit/detail/${item.id}` as any)} - onLongPress={(e) => { - setMenuPos({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }); - setActiveItem(item); - }} - > - - + const renderGridItem = useCallback((item: any) => { + const catName = item.category_id ? categoriesById.get(item.category_id)?.name : null; + return ( + router.push(`/clothes/outfit/detail/${item.id}` as any)} + onLongPress={(e) => { setMenuPos({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }); setActiveItem(item); }}> + + + + + {getClothesCount(item)} 件 + - - {item.name} - - {getClothesCount(item)} 件衣服{categoryName ? ` · ${categoryName}` : ""} - - {itemTags.length > 0 && ( - - {itemTags.slice(0, 3).map((tag) => ( - - {tag!.name} - - ))} - - )} - - - ); - }, - [categoriesById, getClothesCount, getCoverKey, outfitTagIds, tagsById], - ); + {item.name} + {catName && {catName}} + + + ); + }, [categoriesById, getClothesCount, getCoverKey]); return ( - String(item.id)} - renderItem={renderItem} - header={renderHeader()} + ( - + contentContainerStyle={[{ paddingHorizontal: 16, paddingBottom: 120 }, filteredOutfits.length === 0 ? { flexGrow: 1 } : undefined]} + > + {renderHeader()} + {filteredOutfits.length === 0 ? ( + + ) : ( + + {masonry.left.map(renderGridItem)} + {masonry.right.map(renderGridItem)} + )} + + + + + 筛选 { setFilterCategoryId(null); setFilterTagIds([]); setKeyword(""); }}>重置 + + + setFilterCategoryId(null)}>全部 + {categories.filter((c) => c.type === ClothingTargetType.Outfit).map((c) => { + const s = filterCategoryId === c.id; + return ( setFilterCategoryId(s ? null : c.id)}>{c.name}); + })} + + + {tags.filter((t) => t.type === ClothingTargetType.Outfit).map((t) => { + const s = filterTagIds.includes(t.id); + return ( setFilterTagIds((p) => s ? p.filter((id) => id !== t.id) : [...p, t.id])}>{t.name}); + })} + + + + + router.push("/clothes/outfit/edit" as any)}> + + setActiveItem(null)} + items={[ + { icon: , label: "编辑", onPress: () => { const a = activeItem; setActiveItem(null); if (a) router.push(`/clothes/outfit/edit?id=${a.id}` as any); } }, + { icon: , label: "删除", onPress: handleDeleteRequest }, + ]} /> - - - - - - - 搜索搭配 - - { setFilterCategoryId(null); setFilterTagIds([]); setKeyword(""); }} - > - - 重置 - - - - - - - - - - - - - setFilterCategoryId(null)} - > - 全部 - - {categories.filter(c => c.type === ClothingTargetType.Outfit).map(c => { - const selected = filterCategoryId === c.id; - return ( - setFilterCategoryId(selected ? null : c.id)} - > - {c.name} - - ); - })} - - - - - - {tags.filter(t => t.type === ClothingTargetType.Outfit).length > 0 ? ( - - {tags.filter(t => t.type === ClothingTargetType.Outfit).map(t => { - const selected = filterTagIds.includes(t.id); - return ( - setFilterTagIds(prev => selected ? prev.filter(id => id !== t.id) : [...prev, t.id])} - > - {t.name} - - ); - })} - - ) : ( - 暂无标签 - )} - - - - - - - - router.push("/clothes/outfit/edit" as any)} - > - - - - setActiveItem(null)} - > - setActiveItem(null)}> - - { - const item = activeItem; - setActiveItem(null); - if (item) router.push(`/clothes/outfit/edit?id=${item.id}` as any); - }} - > - - 编辑 - - - - 删除 - - { - const item = activeItem; - setActiveItem(null); - if (item) router.push(`/clothes/outfit/detail/${item.id}` as any); - }} - > - - 详细 - - - - - - { if (!v) setDeleteConfirm(null); }}> - - - - 删除搭配 - - 确认删除「{deleteConfirm?.name}」吗?衣服本身不会被删除。 - - - - - - - - - + { if (!v) setDeleteConfirm(null); }}> + 删除搭配确认删除「{deleteConfirm?.name}」吗?衣服本身不会被删除。 + + ); diff --git a/code/app/app-rn/src/app/clothes/detail/[id]/index.tsx b/code/app/app-rn/src/app/clothes/detail/[id]/index.tsx new file mode 100644 index 00000000..c4b5f711 --- /dev/null +++ b/code/app/app-rn/src/app/clothes/detail/[id]/index.tsx @@ -0,0 +1,125 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Text, View } from "react-native"; +import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; +import { Button, Dialog, useThemeColor } from "heroui-native"; +import { PencilIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; +import PageLayout from "@/layouts/PageLayout"; +import LayoutView from "@/components/layout/view"; +import ClothingImage from "@/components/clothing/ClothingImage"; +import EmptyState from "@/components/EmptyStatus"; +import { LoadingOverlay } from "@/components/LoadingOverlay"; +import { useLoading } from "@/hooks/useLoading"; +import { useModuleChatNotifier } from "@/hooks/useModuleChatNotifier"; +import { useClothingStore, useFamilyStore } from "@/store"; +import { ClothingTargetType } from "@/types/clothing"; +import { deleteS3FileQuietly } from "@/utils/file"; + +export default function ClothingDetail() { + const router = useRouter(); + const { id } = useLocalSearchParams<{ id?: string }>(); + const clothesId = id ? Number(id) : 0; + const familyId = useFamilyStore((s) => s.getFamilyId()); + const accentColor = useThemeColor("accent"); + const clothes = useClothingStore((s) => s.clothes); + const categories = useClothingStore((s) => s.categories); + const tags = useClothingStore((s) => s.tags); + const tagRelations = useClothingStore((s) => s.tagRelations); + const fetchClothes = useClothingStore((s) => s.fetchClothes); + const fetchOutfits = useClothingStore((s) => s.fetchOutfits); + const fetchOutfitClothes = useClothingStore((s) => s.fetchOutfitClothes); + const fetchCategories = useClothingStore((s) => s.fetchCategories); + const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); + const fetchTags = useClothingStore((s) => s.fetchTags); + const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); + const deleteClothesBundle = useClothingStore((s) => s.deleteClothesBundle); + const { isLoading, startLoading, stopLoading } = useLoading(); + const notifyModuleOperation = useModuleChatNotifier(); + const [pageLoading, setPageLoading] = useState(true); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + + const item = useMemo(() => clothes.find((c) => c.id === clothesId) ?? null, [clothesId, clothes]); + const categoryName = useMemo(() => { if (!item?.category_id) return "未分类"; return categories.find((c) => c.id === item.category_id)?.name ?? "未分类"; }, [categories, item?.category_id]); + const selectedTags = useMemo(() => { + const ids = new Set(tagRelations.filter((r) => r.target_type === ClothingTargetType.Clothes && r.target_id === clothesId).map((r) => r.tag_id)); + return tags.filter((t) => ids.has(t.id)); + }, [clothesId, tagRelations, tags]); + + const loadData = useCallback(async () => { + if (!familyId || !clothesId) return; setPageLoading(true); + try { await Promise.all([fetchClothes(familyId), fetchOutfits(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Clothes), fetchTagGroups(familyId, ClothingTargetType.Clothes), fetchTags(familyId, ClothingTargetType.Clothes), fetchTagRelations(familyId, ClothingTargetType.Clothes, clothesId)]); } + finally { setPageLoading(false); } + }, [familyId, clothesId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTagGroups, fetchTagRelations, fetchTags]); + useEffect(() => { loadData(); }, [loadData]); + useFocusEffect(useCallback(() => { if (familyId && clothesId) loadData(); }, [familyId, loadData, clothesId])); + + const handleDeleteConfirm = useCallback(async () => { + if (!familyId || !item || isLoading) return; setShowDeleteDialog(false); startLoading("删除中..."); + try { + const state = useClothingStore.getState(); + const refs = state.outfitClothes.filter((r) => r.clothes_id === item.id); + const relatedIds = [...new Set(refs.map((r) => r.outfit_id))]; + const ok = await deleteClothesBundle({ clothesDeletes: [{ id: item.id }], outfitDeletes: relatedIds.map((oid) => ({ id: oid })), outfitClothesDeletes: state.outfitClothes.filter((r) => relatedIds.includes(r.outfit_id)).map((r) => ({ id: r.id })), tagRelationDeletes: state.tagRelations.filter((r) => (r.target_type === ClothingTargetType.Clothes && r.target_id === item.id) || (r.target_type === ClothingTargetType.Outfit && relatedIds.includes(r.target_id))).map((r) => ({ id: r.id })) }); + if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } + deleteS3FileQuietly(item.image_file_key); + state.outfits.filter((o) => relatedIds.includes(o.id)).forEach((o) => deleteS3FileQuietly(o.cover_file_key)); + notifyModuleOperation({ module: "衣服搭配", action: "删除衣服", target: item.name, detail: relatedIds.length > 0 ? `同时删除 ${relatedIds.length} 个关联搭配` : undefined, sourceTable: "b_clothing_clothes", sourceId: item.id }); + stopLoading({ type: "success", message: "已删除" }); router.replace("/clothes/home" as any); + } catch { stopLoading({ type: "error", message: "删除失败" }); } + }, [deleteClothesBundle, familyId, isLoading, item, notifyModuleOperation, router, startLoading, stopLoading]); + + return ( + + + + + {!item && !pageLoading ? ( + + ) : item && (<> + {/* 图片 - 无边距全宽 */} + + + + + {/* 信息区 */} + + 衣服详情 + {item.name} + {!!item.brand && {item.brand}} + + {/* 分类 + 标签数 - 横向排列,不套卡片 */} + + {categoryName} + {selectedTags.length > 0 && ( + + {selectedTags.map((tag) => ( + + {tag.name} + + ))} + + )} + + + + {/* 备注 */} + {!!item.remark && ( + + 备注 + {item.remark} + + )} + )} + + + + + + + + + + 删除衣服确认删除「{item?.name}」吗? + + + ); +} diff --git a/code/app/app-rn/src/app/clothes/edit/index.tsx b/code/app/app-rn/src/app/clothes/edit/index.tsx index 403c4337..78c23717 100644 --- a/code/app/app-rn/src/app/clothes/edit/index.tsx +++ b/code/app/app-rn/src/app/clothes/edit/index.tsx @@ -344,8 +344,9 @@ export default function ClothingEdit() { setSelectedTagIds((prev) => toggleId(prev, item.id))} > @@ -398,8 +399,6 @@ export default function ClothingEdit() { )} - {/* 基础信息 */} - 基础信息 diff --git a/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx b/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx index 50ac49de..215ff11a 100644 --- a/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx +++ b/code/app/app-rn/src/app/clothes/outfit/detail/[id]/index.tsx @@ -1,15 +1,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Text, TouchableOpacity, View } from "react-native"; import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; -import { Button, Card, Dialog } from "heroui-native"; -import { - FolderIcon, - PencilIcon, - RotateCcwIcon, - ShirtIcon, - TagIcon, - Trash2Icon, -} from "lucide-react-native"; +import { Button, Dialog, useThemeColor } from "heroui-native"; +import { PencilIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; import PageLayout from "@/layouts/PageLayout"; import LayoutView from "@/components/layout/view"; import ClothingImage from "@/components/clothing/ClothingImage"; @@ -18,350 +11,141 @@ import { LoadingOverlay } from "@/components/LoadingOverlay"; import { useLoading } from "@/hooks/useLoading"; import { useModuleChatNotifier } from "@/hooks/useModuleChatNotifier"; import { useClothingStore, useFamilyStore } from "@/store"; -import { ClothingOutfitClothes, ClothingTargetType } from "@/types/clothing"; -import { deleteS3FileQuietly } from "@/utils/file"; +import { ClothingTargetType } from "@/types/clothing"; -export default function ClothingOutfitDetail() { +export default function OutfitDetail() { const router = useRouter(); const { id } = useLocalSearchParams<{ id?: string }>(); const outfitId = id ? Number(id) : 0; const familyId = useFamilyStore((s) => s.getFamilyId()); + const accentColor = useThemeColor("accent"); const outfits = useClothingStore((s) => s.outfits); const clothes = useClothingStore((s) => s.clothes); const outfitClothes = useClothingStore((s) => s.outfitClothes); const categories = useClothingStore((s) => s.categories); - const tagGroups = useClothingStore((s) => s.tagGroups); const tags = useClothingStore((s) => s.tags); const tagRelations = useClothingStore((s) => s.tagRelations); const fetchOutfits = useClothingStore((s) => s.fetchOutfits); - const fetchClothes = useClothingStore((s) => s.fetchClothes); const fetchOutfitClothes = useClothingStore((s) => s.fetchOutfitClothes); const fetchCategories = useClothingStore((s) => s.fetchCategories); - const fetchTagGroups = useClothingStore((s) => s.fetchTagGroups); const fetchTags = useClothingStore((s) => s.fetchTags); const fetchTagRelations = useClothingStore((s) => s.fetchTagRelations); + const fetchClothes = useClothingStore((s) => s.fetchClothes); const saveOutfitBundle = useClothingStore((s) => s.saveOutfitBundle); const { isLoading, startLoading, stopLoading } = useLoading(); const notifyModuleOperation = useModuleChatNotifier(); const [pageLoading, setPageLoading] = useState(true); const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const outfit = useMemo( - () => outfits.find((item) => item.id === outfitId) ?? null, - [outfitId, outfits], - ); - const clothesById = useMemo(() => new Map(clothes.map((item) => [item.id, item])), [clothes]); - const details = useMemo( - () => - outfitClothes - .filter((item) => item.outfit_id === outfitId) - .sort((a, b) => a.z_index - b.z_index || a.sort_number - b.sort_number || a.id - b.id), - [outfitClothes, outfitId], - ); - const categoryName = useMemo(() => { - if (!outfit?.category_id) return "未分类"; - return categories.find((item) => item.id === outfit.category_id)?.name ?? "未分类"; - }, [categories, outfit?.category_id]); - const selectedTags = useMemo(() => { - const relationTagIds = new Set( - tagRelations - .filter( - (item) => item.target_type === ClothingTargetType.Outfit && item.target_id === outfitId, - ) - .map((item) => item.tag_id), - ); - return tags.filter((item) => relationTagIds.has(item.id)); - }, [outfitId, tagRelations, tags]); - const coverKey = useMemo(() => { - if (outfit?.cover_file_key) return outfit.cover_file_key; - const first = details[0]; - return first ? clothesById.get(first.clothes_id)?.image_file_key : ""; - }, [clothesById, details, outfit?.cover_file_key]); + const item = useMemo(() => outfits.find((o) => o.id === outfitId) ?? null, [outfitId, outfits]); + const categoryName = useMemo(() => { if (!item?.category_id) return "未分类"; return categories.find((c) => c.id === item.category_id)?.name ?? "未分类"; }, [categories, item?.category_id]); + const details = useMemo(() => outfitClothes.filter((r) => r.outfit_id === outfitId).sort((a, b) => a.z_index - b.z_index || a.sort_number - b.sort_number).map((r) => ({ ...r, clothes: clothes.find((c) => c.id === r.clothes_id) })).filter((r) => r.clothes), [clothes, outfitClothes, outfitId]); + const selectedTags = useMemo(() => { const ids = new Set(tagRelations.filter((r) => r.target_type === ClothingTargetType.Outfit && r.target_id === outfitId).map((r) => r.tag_id)); return tags.filter((t) => ids.has(t.id)); }, [outfitId, tagRelations, tags]); + const getCoverKey = useCallback(() => { if (!item) return ""; if (item.cover_file_key) return item.cover_file_key; return details[0]?.clothes?.image_file_key ?? ""; }, [details, item]); const loadData = useCallback(async () => { - if (!familyId || !outfitId) return; - - setPageLoading(true); - try { - await Promise.all([ - fetchOutfits(familyId), - fetchClothes(familyId), - fetchOutfitClothes(familyId, outfitId), - fetchCategories(familyId, ClothingTargetType.Outfit), - fetchTagGroups(familyId, ClothingTargetType.Outfit), - fetchTags(familyId, ClothingTargetType.Outfit), - fetchTagRelations(familyId, ClothingTargetType.Outfit, outfitId), - ]); - } finally { - setPageLoading(false); - } - }, [ - familyId, - fetchCategories, - fetchClothes, - fetchOutfitClothes, - fetchOutfits, - fetchTagGroups, - fetchTagRelations, - fetchTags, - outfitId, - ]); - - useEffect(() => { - loadData(); - }, [loadData]); - - useFocusEffect( - useCallback(() => { - if (!familyId || !outfitId) return; - loadData(); - }, [familyId, loadData, outfitId]), - ); - - const handleDeleteRequest = useCallback(() => { - if (!familyId || !outfit || isLoading) return; - setShowDeleteDialog(true); - }, [familyId, outfit, isLoading]); + if (!familyId || !outfitId) return; setPageLoading(true); + try { await Promise.all([fetchOutfits(familyId), fetchClothes(familyId), fetchOutfitClothes(familyId), fetchCategories(familyId, ClothingTargetType.Outfit), fetchTags(familyId, ClothingTargetType.Outfit), fetchTagRelations(familyId, ClothingTargetType.Outfit, outfitId)]); } + finally { setPageLoading(false); } + }, [familyId, outfitId, fetchCategories, fetchClothes, fetchOutfitClothes, fetchOutfits, fetchTags, fetchTagRelations]); + useEffect(() => { loadData(); }, [loadData]); + useFocusEffect(useCallback(() => { if (familyId && outfitId) loadData(); }, [familyId, loadData, outfitId])); const handleDeleteConfirm = useCallback(async () => { - if (!familyId || !outfit || isLoading) return; - setShowDeleteDialog(false); - startLoading("删除中..."); + if (!familyId || !item || isLoading) return; setShowDeleteDialog(false); startLoading("删除中..."); try { const state = useClothingStore.getState(); - const currentDetails = state.outfitClothes.filter( - (item) => item.outfit_id === outfit.id, - ); - const relationDeletes = state.tagRelations - .filter( - (item) => - item.target_type === ClothingTargetType.Outfit && item.target_id === outfit.id, - ) - .map((item) => ({ id: item.id })); - const ok = await saveOutfitBundle({ - outfitDeletes: [{ id: outfit.id }], - clothesDeletes: currentDetails.map((item: ClothingOutfitClothes) => ({ - id: item.id, - })), - tagRelationDeletes: relationDeletes, - }); - - if (!ok) { - stopLoading({ type: "error", message: "删除失败" }); - return; - } - - const clothesImageKeys = new Set( - state.clothes.map((item) => item.image_file_key).filter(Boolean), - ); - if (outfit.cover_file_key && !clothesImageKeys.has(outfit.cover_file_key)) { - deleteS3FileQuietly(outfit.cover_file_key); - } - await Promise.all([ - fetchOutfits(familyId), - fetchOutfitClothes(familyId), - fetchTagRelations(familyId), - ]); - notifyModuleOperation({ - module: "衣服搭配", - action: "删除搭配", - target: outfit.name, - detail: `${currentDetails.length} 件衣服`, - sourceTable: "b_clothing_outfit", - sourceId: outfit.id, - }); - stopLoading({ type: "success", message: "搭配已删除" }); - router.replace("/clothes/outfit" as any); - } catch { - stopLoading({ type: "error", message: "删除失败" }); - } - }, [ - familyId, - fetchOutfitClothes, - fetchOutfits, - fetchTagRelations, - isLoading, - notifyModuleOperation, - outfit, - router, - saveOutfitBundle, - startLoading, - stopLoading, - ]); - - const renderDetailClothes = useCallback( - (detail: ClothingOutfitClothes) => { - const item = clothesById.get(detail.clothes_id); - - return ( - router.push(`/clothes/edit?id=${detail.clothes_id}` as any)} - > - - - - {item?.name ?? "衣服不存在"} - - {!!item?.brand && ( - - {item.brand} - - )} - - - ); - }, - [clothesById, router], - ); + const d = state.outfitClothes.filter((r) => r.outfit_id === item.id); + const ok = await saveOutfitBundle({ outfitDeletes: [{ id: item.id }], clothesDeletes: d.map((r) => ({ id: r.id })), tagRelationDeletes: state.tagRelations.filter((r) => r.target_type === ClothingTargetType.Outfit && r.target_id === item.id).map((r) => ({ id: r.id })) }); + if (!ok) { stopLoading({ type: "error", message: "删除失败" }); return; } + notifyModuleOperation({ module: "衣服搭配", action: "删除搭配", target: item.name, detail: `${d.length} 件衣服`, sourceTable: "b_clothing_outfit", sourceId: item.id }); + stopLoading({ type: "success", message: "已删除" }); router.replace("/clothes/outfit" as any); + } catch { stopLoading({ type: "error", message: "删除失败" }); } + }, [familyId, isLoading, item, notifyModuleOperation, router, saveOutfitBundle, startLoading, stopLoading]); return ( - + - - {!outfit && !pageLoading ? ( - - ) : ( - outfit && ( - <> - - - + + {!item && !pageLoading ? ( + + ) : item && (<> + {/* 封面图 */} + + + - - - - - - {outfit.name} - - {details.length} 件衣服 - - - - - - - - {categoryName} - + {/* 信息区 */} + + 搭配详情 + {item.name} - {selectedTags.length > 0 && ( - - - - 标签 - - - {selectedTags.map((tag) => { - const group = tagGroups.find((item) => item.id === tag.tag_group_id); - return ( - - - {group ? `${group.name}/` : ""} - {tag.name} - - - ); - })} - - - )} + {/* 统计 + 标签 - 横向排列 */} + + {categoryName} + {details.length} 件衣服 + {selectedTags.map((tag) => ( + + {tag.name} + + ))} + + - {!!outfit.remark && ( - {outfit.remark} - )} - - - - - - - - 包含衣服 - - - - {details.length > 0 ? ( - details.map(renderDetailClothes) - ) : ( - 暂无衣服明细 - )} - - - - - - + {/* 包含衣服 - 2列网格 */} + + 包含衣服 · {details.length} 件 + {details.length > 0 ? ( + + {(() => { + const left = details.filter((_, i) => i % 2 === 0); + const right = details.filter((_, i) => i % 2 === 1); + return ( + <> + {left.map((d) => ( + router.push(`/clothes/detail/${d.clothes_id}` as any)}> + + + {d.z_index} + + {d.clothes?.name} + + ))} + {right.map((d) => ( + router.push(`/clothes/detail/${d.clothes_id}` as any)}> + + + {d.z_index} + + {d.clothes?.name} + + ))} + + ); + })()} - - ) - )} + ) : (暂无)} + + + {/* 备注 */} + {!!item.remark && ( + + 备注 + {item.remark} + + )} + )} - - - - - 删除搭配 - - 确认删除「{outfit?.name}」吗?衣服本身不会被删除。 - - - - - - - - + + + + + + 删除搭配确认删除「{item?.name}」吗?衣服本身不会被删除。 ); diff --git a/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx b/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx index ad80c788..5de8f661 100644 --- a/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx +++ b/code/app/app-rn/src/app/clothes/outfit/edit/index.tsx @@ -1,17 +1,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { PanResponder, Text, TouchableOpacity, View } from "react-native"; +import { PanResponder, ScrollView, Text, TouchableOpacity, View } from "react-native"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { captureRef } from "react-native-view-shot"; -import { Button, Card, Input, Label, TextArea, TextField } from "heroui-native"; +import { Button, Card, Input, Label, TextArea, TextField, useThemeColor } from "heroui-native"; import { BringToFrontIcon, ImageIcon, MinusIcon, PlusIcon, + RotateCcwIcon, SaveIcon, + SearchIcon, SendToBackIcon, Trash2Icon, - XIcon, } from "lucide-react-native"; import { toast } from "sonner-native"; import PageLayout from "@/layouts/PageLayout"; @@ -145,10 +146,12 @@ export default function ClothingOutfitEdit() { const editId = id ? Number(id) : 0; const isEdit = editId > 0; const familyId = useFamilyStore((s) => s.getFamilyId()); + const accentColor = useThemeColor("accent"); const clothes = useClothingStore((s) => s.clothes); const categories = useClothingStore((s) => s.categories); const tagGroups = useClothingStore((s) => s.tagGroups); const tags = useClothingStore((s) => s.tags); + const tagRelations = useClothingStore((s) => s.tagRelations); const fetchClothes = useClothingStore((s) => s.fetchClothes); const fetchOutfits = useClothingStore((s) => s.fetchOutfits); const fetchOutfitClothes = useClothingStore((s) => s.fetchOutfitClothes); @@ -172,6 +175,35 @@ export default function ClothingOutfitEdit() { const [pageLoading, setPageLoading] = useState(true); const [capturingCover, setCapturingCover] = useState(false); const [showClothesSheet, setShowClothesSheet] = useState(false); + const [clothesKeyword, setClothesKeyword] = useState(""); + const [clothesFilterCatId, setClothesFilterCatId] = useState(null); + const [clothesFilterTagIds, setClothesFilterTagIds] = useState([]); + + const clothesCategories = useMemo(() => categories.filter((c) => c.type === ClothingTargetType.Clothes), [categories]); + const clothesTags = useMemo(() => tags.filter((t) => t.type === ClothingTargetType.Clothes), [tags]); + + const clothesTagMap = useMemo(() => { + const map = new Map>(); + tagRelations.filter((r) => r.target_type === ClothingTargetType.Clothes).forEach((r) => { + const s = map.get(r.target_id) ?? new Set(); s.add(r.tag_id); map.set(r.target_id, s); + }); + return map; + }, [tagRelations]); + + const filteredClothes = useMemo(() => { + const kw = clothesKeyword.trim().toLowerCase(); + return clothes.filter((item) => { + if (clothesFilterCatId && item.category_id !== clothesFilterCatId) return false; + if (clothesFilterTagIds.length > 0) { + const ids = clothesTagMap.get(item.id); + if (!ids || !clothesFilterTagIds.every((tid) => ids.has(tid))) return false; + } + if (!kw) return true; + return `${item.name} ${item.brand ?? ""}`.toLowerCase().includes(kw); + }); + }, [clothes, clothesKeyword, clothesFilterCatId, clothesFilterTagIds, clothesTagMap]); + + const clothesFilterCount = useMemo(() => { let c = 0; if (clothesFilterCatId) c++; if (clothesFilterTagIds.length > 0) c++; return c; }, [clothesFilterCatId, clothesFilterTagIds]); const clothesById = useMemo(() => new Map(clothes.map((item) => [item.id, item])), [clothes]); const outfitCategories = useMemo( @@ -393,8 +425,9 @@ export default function ClothingOutfitEdit() { setSelectedTagIds((prev) => toggleId(prev, item.id))} > @@ -692,16 +725,16 @@ export default function ClothingOutfitEdit() { {/* 画布操作栏 */} {selectedItem && ( @@ -720,13 +753,12 @@ export default function ClothingOutfitEdit() { + + + + + + + {/* FAB 新增 */} + router.push("/meal/dish/edit" as any)} + > + + + + {/* 长按菜单 */} + setActiveItem(null)} + items={[ + { icon: , label: "编辑", onPress: () => { if (activeItem) handleEdit(activeItem); } }, + { icon: , label: "停用", onPress: () => { const item = activeItem; setActiveItem(null); if (item) setDeleteConfirm(item); } }, + ]} + /> + + {/* 停用确认 */} + { if (!v) setDeleteConfirm(null); }}> + + + + 停用菜品 + + 确认停用「{deleteConfirm?.name}」吗?停用后菜品将从列表隐藏,历史餐食安排不受影响。 + + + + + + + + + + + + ); +} diff --git a/code/app/app-rn/src/app/meal/(tabs)/home/index.tsx b/code/app/app-rn/src/app/meal/(tabs)/home/index.tsx new file mode 100644 index 00000000..257d8815 --- /dev/null +++ b/code/app/app-rn/src/app/meal/(tabs)/home/index.tsx @@ -0,0 +1,680 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Text, TouchableOpacity, View } from "react-native"; +import { router } from "expo-router"; +import dayjs from "dayjs"; +import { Button } from "heroui-native"; +import { + CalendarDaysIcon, + CheckIcon, + MessageSquareIcon, + PlusIcon, + Trash2Icon, + UserIcon, + XIcon, +} from "lucide-react-native"; +import { toast } from "sonner-native"; +import BottomSheet from "@/components/BottomSheet"; +import EmptyState from "@/components/EmptyStatus"; +import LayoutView from "@/components/layout/view"; +import MealDishImage from "@/components/meal/MealDishImage"; +import { LoadingOverlay } from "@/components/LoadingOverlay"; +import { useLoading } from "@/hooks/useLoading"; +import { getUniqueIdApi, loadDataApi } from "@/request/api"; +import { useFamilyStore, useMealStore, useUserStore } from "@/store"; +import { MealPlanLogType } from "@/types/meal"; + +// ------- 类型 ------- + +interface FamilyMember { + user_id: number; + nickname: string; +} + +interface TimelineEntry { + id: number; + type: MealPlanLogType; + content: string; + ref_id?: number | null; + create_time: string; + create_by: number; + userNickname: string; + dishName?: string; +} + +interface SlotPlanData { + plan: any; + items: any[]; + timeline: TimelineEntry[]; +} + +// ------- 辅助函数 ------- + +function getLogLabel(type: MealPlanLogType): string { + switch (type) { + case MealPlanLogType.Plan: + return "安排了这顿饭"; + case MealPlanLogType.AddDish: + return "加了"; + case MealPlanLogType.RemoveDish: + return "移除了"; + case MealPlanLogType.Remark: + return "留了备注"; + case MealPlanLogType.SetCook: + return "设置了做饭人"; + default: + return ""; + } +} + +// ------- 页面 ------- + +export default function MealHomePage() { + const familyId = useFamilyStore((s) => s.getFamilyId()); + const userId = useUserStore((s) => s.getUserId()); + const dishes = useMealStore((s) => s.dishes); + const slots = useMealStore((s) => s.slots); + const plans = useMealStore((s) => s.plans); + const fetchDishes = useMealStore((s) => s.fetchDishes); + const fetchCategories = useMealStore((s) => s.fetchCategories); + const fetchTags = useMealStore((s) => s.fetchTags); + const fetchDishTags = useMealStore((s) => s.fetchDishTags); + const fetchSlots = useMealStore((s) => s.fetchSlots); + const fetchPlansByDate = useMealStore((s) => s.fetchPlansByDate); + const savePlanItemChanges = useMealStore((s) => s.savePlanItemChanges); + const savePlanLogChanges = useMealStore((s) => s.savePlanLogChanges); + const savePlanChanges = useMealStore((s) => s.savePlanChanges); + const savePlanBundle = useMealStore((s) => s.savePlanBundle); + const recordFeed = useMealStore((s) => s.recordFeed); + const { isLoading, startLoading, stopLoading } = useLoading(); + + const today = dayjs().format("YYYY-MM-DD"); + const tomorrow = dayjs().add(1, "day").format("YYYY-MM-DD"); + + const [planDate, setPlanDate] = useState(today); + const [showDateSheet, setShowDateSheet] = useState(false); + const [datePickerValue, setDatePickerValue] = useState(today); + const [members, setMembers] = useState([]); + const [allPlanItems, setAllPlanItems] = useState([]); + const [allPlanLogs, setAllPlanLogs] = useState([]); + const [pageLoading, setPageLoading] = useState(true); + const [expandedTimelines, setExpandedTimelines] = useState>(new Set()); + + const isToday = planDate === today; + const isTomorrow = planDate === tomorrow; + + // 按 slot 分组 plan 数据 + const slotPlanMap = useMemo(() => { + const map = new Map(); + + // 先按计划聚合 items 和 logs + const itemsByPlan = new Map(); + allPlanItems.forEach((item) => { + const list = itemsByPlan.get(item.plan_id) ?? []; + list.push(item); + itemsByPlan.set(item.plan_id, list); + }); + + const logsByPlan = new Map(); + allPlanLogs.forEach((log) => { + const list = logsByPlan.get(log.plan_id) ?? []; + const member = members.find((m) => m.user_id === log.create_by); + const entry: TimelineEntry = { + id: log.id, + type: log.type, + content: log.content ?? "", + ref_id: log.ref_id, + create_time: log.create_time ?? "", + create_by: log.create_by ?? 0, + userNickname: member?.nickname ?? `用户${log.create_by}`, + }; + + // type=2/3 补 dishName + if ((log.type === MealPlanLogType.AddDish || log.type === MealPlanLogType.RemoveDish) && log.ref_id) { + const planItem = allPlanItems.find((pi) => pi.id === log.ref_id); + if (planItem) { + const dish = dishes.find((d) => d.id === planItem.dish_id); + entry.dishName = dish?.name ?? "未知菜品"; + } + } + + list.push(entry); + logsByPlan.set(log.plan_id, list); + }); + + // 构建 slot -> plan data + slots.forEach((slot) => { + const plan = plans.find((p) => p.meal_slot_id === slot.id); + if (plan) { + map.set(slot.id, { + plan, + items: (itemsByPlan.get(plan.id) ?? []).map((item) => { + const dish = dishes.find((d) => d.id === item.dish_id); + return { ...item, dish }; + }), + timeline: (logsByPlan.get(plan.id) ?? []).sort( + (a, b) => new Date(a.create_time).getTime() - new Date(b.create_time).getTime(), + ), + }); + } + }); + + return map; + }, [slots, plans, allPlanItems, allPlanLogs, dishes, members]); + + // ------- 数据加载 ------- + + const loadData = useCallback(async () => { + if (!familyId) return; + setPageLoading(true); + try { + await Promise.all([ + fetchDishes(familyId), + fetchCategories(familyId), + fetchTags(familyId), + fetchDishTags(familyId), + fetchSlots(familyId), + fetchPlansByDate(familyId, planDate), + ]); + + // 加载所有 plan 的 items 和 logs(使用直接 API 避免 store 覆盖问题) + const state = useMealStore.getState(); + const currentPlans = state.plans; + const planIds = currentPlans.map((p) => p.id); + + if (planIds.length > 0) { + const [itemsRes, logsRes, memberRes] = await Promise.all([ + loadDataApi({ + view_name: "b_meal_plan_item", + search_condition: `family_id = ? and plan_id in (${planIds.map(() => "?").join(",")})`, + search_columns: [], + order_by: "sort_number asc, id asc", + args: [familyId, ...planIds], + }), + loadDataApi({ + view_name: "b_meal_plan_log", + search_condition: `family_id = ? and plan_id in (${planIds.map(() => "?").join(",")})`, + search_columns: [], + order_by: "create_time asc, id asc", + args: [familyId, ...planIds], + }), + loadDataApi({ + view_name: "b_family_member", + search_condition: "family_id = ?", + search_columns: ["user_id", "nickname"], + order_by: "id asc", + args: [familyId], + }), + ]); + + if (itemsRes.isSuccess) setAllPlanItems(itemsRes.data ?? []); + if (logsRes.isSuccess) setAllPlanLogs(logsRes.data ?? []); + if (memberRes.isSuccess) setMembers((memberRes.data as FamilyMember[]) ?? []); + } else { + setAllPlanItems([]); + setAllPlanLogs([]); + // 依然加载家庭成员 + const memberRes = await loadDataApi({ + view_name: "b_family_member", + search_condition: "family_id = ?", + search_columns: ["user_id", "nickname"], + order_by: "id asc", + args: [familyId], + }); + if (memberRes.isSuccess) setMembers((memberRes.data as FamilyMember[]) ?? []); + } + } finally { + setPageLoading(false); + } + }, [ + familyId, planDate, fetchDishes, fetchCategories, fetchTags, fetchDishTags, + fetchSlots, fetchPlansByDate, + ]); + + useEffect(() => { + loadData(); + }, [loadData]); + + // ------- 交互 ------- + + const handleOpenDateSheet = useCallback(() => { + setDatePickerValue(planDate); + setShowDateSheet(true); + }, [planDate]); + + const handleConfirmDate = useCallback(() => { + if (dayjs(datePickerValue).isValid()) { + setPlanDate(datePickerValue); + setShowDateSheet(false); + } else { + toast.error("日期格式无效"); + } + }, [datePickerValue]); + + const handleRemoveDish = useCallback( + async (planId: number, itemId: number, dishName: string) => { + if (!familyId) return; + startLoading("移除中..."); + try { + const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; + + const res = await savePlanItemChanges({ + deletes: [{ id: itemId }], + }); + + if (!res) { + stopLoading({ type: "error", message: "移除失败" }); + return; + } + + // 写入 type=3 日志 + if (logId) { + await savePlanLogChanges({ + inserts: [{ + id: logId, + family_id: familyId, + plan_id: planId, + type: MealPlanLogType.RemoveDish, + ref_id: itemId, + create_by: userId, + }], + }); + } + + await loadData(); + stopLoading({ type: "success", message: `已移除${dishName}` }); + } catch { + stopLoading({ type: "error", message: "移除失败" }); + } + }, + [familyId, loadData, savePlanItemChanges, savePlanLogChanges, startLoading, stopLoading, userId], + ); + + const handleDeleteRemark = useCallback( + async (planId: number, log: TimelineEntry) => { + if (log.create_by !== userId) { + toast.error("只能删除自己的备注"); + return; + } + if (!familyId) return; + startLoading("删除中..."); + try { + await savePlanLogChanges({ deletes: [{ id: log.id }] }); + await loadData(); + stopLoading({ type: "success", message: "备注已删除" }); + } catch { + stopLoading({ type: "error", message: "删除失败" }); + } + }, + [familyId, loadData, savePlanLogChanges, startLoading, stopLoading, userId], + ); + + const handleRemoveRemarkPlan = useCallback( + async (planId: number) => { + if (!familyId) return; + startLoading("操作中..."); + try { + await savePlanChanges({ updates: [{ id: planId, remark: "" }] }); + await loadData(); + stopLoading({ type: "success", message: "备注已清除" }); + } catch { + stopLoading({ type: "error", message: "操作失败" }); + } + }, + [familyId, loadData, savePlanChanges, startLoading, stopLoading], + ); + + // ------- 复制历史餐食到今日 ------- + + const handleCopyPlan = useCallback( + async (planId: number, mealSlotId: number) => { + if (!familyId) return; + startLoading("复制中..."); + try { + // 查看今日是否已有同一餐段的 plan + const todayPlans = await loadDataApi({ + view_name: "b_meal_plan", + search_condition: "family_id = ? and plan_date = ? and meal_slot_id = ?", + search_columns: ["id"], + order_by: "id asc", + args: [familyId, today, mealSlotId], + }); + + const sourceItems = allPlanItems.filter((item) => item.plan_id === planId); + if (sourceItems.length === 0) { + stopLoading({ type: "error", message: "无菜品可复制" }); + return; + } + + const newPlanId = todayPlans.data?.[0]?.id ?? (await getUniqueIdApi(1)).data?.[0] ?? 0; + if (!newPlanId) { + stopLoading({ type: "error", message: "生成计划 ID 失败" }); + return; + } + + const itemIds = (await getUniqueIdApi(sourceItems.length)).data ?? []; + const logId = (await getUniqueIdApi(1)).data?.[0] ?? 0; + + const newItems = sourceItems.map((item, i) => ({ + id: itemIds[i], + family_id: familyId, + plan_id: newPlanId, + dish_id: item.dish_id, + quantity: item.quantity ?? 1, + sort_number: i, + })); + + // 创建 plan + items + log + const isNewPlan = !todayPlans.data?.[0]; + await savePlanBundle({ + planInserts: isNewPlan + ? [{ id: newPlanId, family_id: familyId, plan_date: today, meal_slot_id: mealSlotId }] + : [], + planUpdates: [], + planItemInserts: newItems, + planItemUpdates: [], + planItemDeletes: [], + planLogInserts: [ + ...(isNewPlan && logId + ? [{ id: logId, family_id: familyId, plan_id: newPlanId, type: MealPlanLogType.Plan, create_by: userId }] + : []), + ], + }); + + await loadData(); + stopLoading({ type: "success", message: "已复制到今天" }); + } catch { + stopLoading({ type: "error", message: "复制失败" }); + } + }, + [familyId, allPlanItems, today, startLoading, stopLoading, userId], + ); + + const toggleTimeline = useCallback((planId: number) => { + setExpandedTimelines((prev) => { + const next = new Set(prev); + if (next.has(planId)) next.delete(planId); + else next.add(planId); + return next; + }); + }, []); + + // ------- 渲染 ------- + + const renderTimelineEntry = useCallback( + (entry: TimelineEntry, planId: number) => { + const isRemark = entry.type === MealPlanLogType.Remark; + const isOwnRemark = isRemark && entry.create_by === userId; + + let label = `${entry.userNickname} ${getLogLabel(entry.type)}`; + if (entry.type === MealPlanLogType.AddDish || entry.type === MealPlanLogType.RemoveDish) { + label += ` ${entry.dishName ?? "一道菜"}`; + } + + return ( + + + + + {label} + {isRemark && entry.content ? `:「${entry.content}」` : ""} + + {isOwnRemark && ( + handleDeleteRemark(planId, entry)} + > + + + )} + + + ); + }, + [handleDeleteRemark, userId], + ); + + const getCookName = useCallback( + (cookById?: number | null) => { + if (!cookById) return null; + return members.find((m) => m.user_id === cookById)?.nickname ?? `用户${cookById}`; + }, + [members], + ); + + if (slots.length === 0 && !pageLoading) { + return ( + + + + + + + ); + } + + return ( + + + {/* ======== 日期切换栏 ======== */} + + setPlanDate(today)} + > + + 今天 + + + setPlanDate(tomorrow)} + > + + 明天 + + + + + + {isToday || isTomorrow ? planDate : planDate} + + + + + {/* ======== 餐段卡片 ======== */} + + {slots.map((slot) => { + const data = slotPlanMap.get(slot.id); + const hasPlan = !!data; + const timeline = data?.timeline ?? []; + const isTimelineExpanded = expandedTimelines.has(data?.plan.id ?? 0); + const cookName = getCookName(data?.plan.cook_by); + + return ( + + {/* 头部 */} + + + + {slot.name} + {cookName && ( + + + {cookName} + + )} + + + router.push( + `/meal/plan/edit?planDate=${planDate}&mealSlotId=${slot.id}` as any, + ) + } + > + + + + + {/* 菜品列表 */} + {hasPlan && data.items.length > 0 ? ( + + {data.items.map((item: any) => ( + { + if (item.dish?.id) { + router.push(`/meal/dish/detail/${item.dish.id}` as any); + } + }} + > + + {item.dish && ( + + )} + + + + {item.dish ? item.dish.name : "未知菜品"} + + {item.quantity > 1 && ( + x{item.quantity} + )} + + { + e.stopPropagation(); + const dishName = item.dish?.name ?? ""; + handleRemoveDish(data.plan.id, item.id, dishName); + }} + > + + + + ))} + + {/* 备注 */} + {data.plan.remark ? ( + + + {data.plan.remark} + handleRemoveRemarkPlan(data.plan.id)} + > + + + + ) : null} + + ) : ( + + 暂未安排 + + router.push( + `/meal/plan/edit?planDate=${planDate}&mealSlotId=${slot.id}` as any, + ) + } + > + + 添加菜品 + + + )} + + {/* 时间轴 */} + {timeline.length > 0 && hasPlan && ( + <> + toggleTimeline(data.plan.id)} + > + + {isTimelineExpanded ? "收起" : `${timeline.length} 条记录`} + + + {isTimelineExpanded && ( + + {timeline.map((entry) => renderTimelineEntry(entry, data.plan.id))} + + )} + + )} + + {/* 历史日期:复制到今天 */} + {!isToday && hasPlan && ( + handleCopyPlan(data.plan.id, slot.id)} + > + 复制到「今天」 + + )} + + ); + })} + + {slots.length > 0 && + !pageLoading && + slots.every((slot) => !slotPlanMap.has(slot.id)) && ( + + + + )} + + + + {/* ======== 日期选择 BottomSheet ======== */} + + + + + 选择日期 + + 日期 (YYYY-MM-DD) + + + + + + + + + + + + + ); +} diff --git a/code/app/app-rn/src/app/meal/(tabs)/manage/index.tsx b/code/app/app-rn/src/app/meal/(tabs)/manage/index.tsx new file mode 100644 index 00000000..0990530b --- /dev/null +++ b/code/app/app-rn/src/app/meal/(tabs)/manage/index.tsx @@ -0,0 +1,775 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Text, TouchableOpacity, View } from "react-native"; +import { Button, Dialog, Input, Label, TextField } from "heroui-native"; +import { + ChevronDownIcon, + ChevronRightIcon, + ChevronUpIcon, + ClockIcon, + FoldersIcon, + GripVerticalIcon, + PlusIcon, + RotateCcwIcon, + TagIcon, + Trash2Icon, +} from "lucide-react-native"; +import BottomSheet from "@/components/BottomSheet"; +import EmptyState from "@/components/EmptyStatus"; +import LayoutView from "@/components/layout/view"; +import { LoadingOverlay } from "@/components/LoadingOverlay"; +import { useLoading } from "@/hooks/useLoading"; +import { getUniqueIdApi } from "@/request/api"; +import { useFamilyStore, useMealStore } from "@/store"; +import { MealCategory, MealSlot, MealStatus, MealTag } from "@/types/meal"; + +// ------- 表单类型 ------- + +interface NameForm { + name: string; +} + +interface TagForm { + name: string; + color: string; +} + +const COLORS = [ + "#EF4444", "#F97316", "#F59E0B", "#84CC16", "#22C55E", + "#14B8A6", "#06B6D4", "#3B82F6", "#6366F1", "#8B5CF6", + "#A855F7", "#D946EF", "#EC4899", "#F43F5E", "#78716C", +]; + +// ------- 页面 ------- + +export default function MealManagePage() { + const familyId = useFamilyStore((s) => s.getFamilyId()); + const categories = useMealStore((s) => s.categories); + const tags = useMealStore((s) => s.tags); + const slots = useMealStore((s) => s.slots); + const fetchCategories = useMealStore((s) => s.fetchCategories); + const fetchTags = useMealStore((s) => s.fetchTags); + const fetchSlots = useMealStore((s) => s.fetchSlots); + const saveCategoryChanges = useMealStore((s) => s.saveCategoryChanges); + const saveTagChanges = useMealStore((s) => s.saveTagChanges); + const saveSlotChanges = useMealStore((s) => s.saveSlotChanges); + const { isLoading, startLoading, stopLoading } = useLoading(); + + // ---- 分类 ---- + const [showCategorySheet, setShowCategorySheet] = useState(false); + const [categoryForm, setCategoryForm] = useState({ name: "" }); + const [editingCategory, setEditingCategory] = useState(null); + const [categoryDeleteConfirm, setCategoryDeleteConfirm] = useState(null); + + // ---- 标签 ---- + const [showTagSheet, setShowTagSheet] = useState(false); + const [tagForm, setTagForm] = useState({ name: "", color: "" }); + const [editingTag, setEditingTag] = useState(null); + const [tagDeleteConfirm, setTagDeleteConfirm] = useState(null); + const [editingParentId, setEditingParentId] = useState(null); + const [expandedParents, setExpandedParents] = useState>(new Set()); + + // ---- 餐段 ---- + const [showSlotSheet, setShowSlotSheet] = useState(false); + const [slotForm, setSlotForm] = useState({ name: "" }); + const [editingSlot, setEditingSlot] = useState(null); + const [slotDeleteConfirm, setSlotDeleteConfirm] = useState(null); + + // 数据分组 + const parentTags = useMemo(() => tags.filter((t) => !t.parent_id), [tags]); + + const childTagsByParent = useMemo(() => { + const map = new Map(); + tags + .filter((t) => !!t.parent_id) + .forEach((tag) => { + const group = map.get(tag.parent_id!) ?? []; + group.push(tag); + map.set(tag.parent_id!, group); + }); + return map; + }, [tags]); + + // ------- 数据加载 ------- + + const loadData = useCallback(async () => { + if (!familyId) return; + await Promise.all([fetchCategories(familyId), fetchTags(familyId), fetchSlots(familyId)]); + }, [familyId, fetchCategories, fetchSlots, fetchTags]); + + useEffect(() => { + if (!familyId) return; + loadData(); + }, [familyId]); + + // ------- 分类操作 ------- + + const handleOpenCategoryForm = useCallback((category?: MealCategory) => { + if (category) { + setCategoryForm({ name: category.name }); + setEditingCategory(category); + } else { + setCategoryForm({ name: "" }); + setEditingCategory(null); + } + setShowCategorySheet(true); + }, []); + + const handleSaveCategory = useCallback(async () => { + if (!familyId || !categoryForm.name.trim()) return; + startLoading("保存中..."); + + try { + if (editingCategory) { + await saveCategoryChanges({ + updates: [{ id: editingCategory.id, name: categoryForm.name.trim() }], + }); + } else { + const id = (await getUniqueIdApi(1)).data?.[0]; + if (!id) { + stopLoading({ type: "error", message: "获取 ID 失败" }); + return; + } + await saveCategoryChanges({ + inserts: [{ id, family_id: familyId, name: categoryForm.name.trim(), status: MealStatus.Enabled }], + }); + } + setShowCategorySheet(false); + await loadData(); + stopLoading({ type: "success", message: editingCategory ? "已更新" : "已添加" }); + } catch { + stopLoading({ type: "error", message: "保存失败" }); + } + }, [categoryForm.name, editingCategory, familyId, loadData, saveCategoryChanges, startLoading, stopLoading]); + + const handleDeleteCategory = useCallback(async () => { + if (!categoryDeleteConfirm) return; + startLoading("删除中..."); + try { + await saveCategoryChanges({ deletes: [{ id: categoryDeleteConfirm.id }] }); + setCategoryDeleteConfirm(null); + await loadData(); + stopLoading({ type: "success", message: "已删除" }); + } catch { + stopLoading({ type: "error", message: "删除失败" }); + } + }, [categoryDeleteConfirm, loadData, saveCategoryChanges, startLoading, stopLoading]); + + // ------- 标签操作 ------- + + const handleOpenTagForm = useCallback((parentId: number | null, tag?: MealTag) => { + setEditingParentId(parentId); + if (tag) { + setTagForm({ name: tag.name, color: tag.color ?? "" }); + setEditingTag(tag); + } else { + setTagForm({ name: "", color: "" }); + setEditingTag(null); + } + setShowTagSheet(true); + }, []); + + const handleSaveTag = useCallback(async () => { + if (!familyId || !tagForm.name.trim()) return; + startLoading("保存中..."); + + try { + if (editingTag) { + await saveTagChanges({ + updates: [{ id: editingTag.id, name: tagForm.name.trim(), color: tagForm.color }], + }); + } else { + const id = (await getUniqueIdApi(1)).data?.[0]; + if (!id) { + stopLoading({ type: "error", message: "获取 ID 失败" }); + return; + } + await saveTagChanges({ + inserts: [{ + id, + family_id: familyId, + name: tagForm.name.trim(), + color: tagForm.color, + parent_id: editingParentId, + status: MealStatus.Enabled, + }], + }); + } + setShowTagSheet(false); + await loadData(); + stopLoading({ type: "success", message: editingTag ? "已更新" : "已添加" }); + } catch { + stopLoading({ type: "error", message: "保存失败" }); + } + }, [editingParentId, editingTag, familyId, loadData, saveTagChanges, startLoading, stopLoading, tagForm]); + + const handleDeleteTag = useCallback(async () => { + if (!tagDeleteConfirm) return; + startLoading("删除中..."); + try { + // 如果是父标签,同时删除子标签 + const isParent = !tagDeleteConfirm.parent_id; + const childTags = isParent ? childTagsByParent.get(tagDeleteConfirm.id) ?? [] : []; + const deletes = [{ id: tagDeleteConfirm.id }, ...childTags.map((t) => ({ id: t.id }))]; + await saveTagChanges({ deletes }); + setTagDeleteConfirm(null); + await loadData(); + stopLoading({ type: "success", message: isParent ? "父标签及子标签已删除" : "已删除" }); + } catch { + stopLoading({ type: "error", message: "删除失败" }); + } + }, [childTagsByParent, loadData, saveTagChanges, startLoading, stopLoading, tagDeleteConfirm]); + + const toggleParentExpanded = useCallback((parentId: number) => { + setExpandedParents((prev) => { + const next = new Set(prev); + if (next.has(parentId)) next.delete(parentId); + else next.add(parentId); + return next; + }); + }, []); + + // ------- 餐段操作 ------- + + const handleOpenSlotForm = useCallback((slot?: MealSlot) => { + if (slot) { + setSlotForm({ name: slot.name }); + setEditingSlot(slot); + } else { + setSlotForm({ name: "" }); + setEditingSlot(null); + } + setShowSlotSheet(true); + }, []); + + const handleSaveSlot = useCallback(async () => { + if (!familyId || !slotForm.name.trim()) return; + startLoading("保存中..."); + + try { + if (editingSlot) { + await saveSlotChanges({ + updates: [{ id: editingSlot.id, name: slotForm.name.trim() }], + }); + } else { + const id = (await getUniqueIdApi(1)).data?.[0]; + if (!id) { + stopLoading({ type: "error", message: "获取 ID 失败" }); + return; + } + await saveSlotChanges({ + inserts: [{ id, family_id: familyId, name: slotForm.name.trim(), sort_number: slots.length, status: MealStatus.Enabled }], + }); + } + setShowSlotSheet(false); + await loadData(); + stopLoading({ type: "success", message: editingSlot ? "已更新" : "已添加" }); + } catch { + stopLoading({ type: "error", message: "保存失败" }); + } + }, [editingSlot, familyId, loadData, saveSlotChanges, slotForm.name, slots.length, startLoading, stopLoading]); + + const handleToggleSlotStatus = useCallback(async (slot: MealSlot) => { + startLoading("操作中..."); + try { + const newStatus = slot.status === MealStatus.Enabled ? MealStatus.Disabled : MealStatus.Enabled; + await saveSlotChanges({ updates: [{ id: slot.id, status: newStatus }] }); + await loadData(); + stopLoading({ type: "success", message: newStatus === MealStatus.Enabled ? "已启用" : "已停用" }); + } catch { + stopLoading({ type: "error", message: "操作失败" }); + } + }, [loadData, saveSlotChanges, startLoading, stopLoading]); + + const handleMoveSlot = useCallback(async (index: number, direction: "up" | "down") => { + const targetIndex = direction === "up" ? index - 1 : index + 1; + if (targetIndex < 0 || targetIndex >= slots.length) return; + + const current = slots[index]; + const target = slots[targetIndex]; + + await saveSlotChanges({ + updates: [ + { id: current.id, sort_number: target.sort_number }, + { id: target.id, sort_number: current.sort_number }, + ], + }); + await loadData(); + }, [loadData, saveSlotChanges, slots]); + + const handleDeleteSlot = useCallback(async () => { + if (!slotDeleteConfirm) return; + startLoading("删除中..."); + try { + await saveSlotChanges({ deletes: [{ id: slotDeleteConfirm.id }] }); + setSlotDeleteConfirm(null); + await loadData(); + stopLoading({ type: "success", message: "已删除" }); + } catch { + stopLoading({ type: "error", message: "删除失败" }); + } + }, [loadData, saveSlotChanges, slotDeleteConfirm, startLoading, stopLoading]); + + // ------- 渲染 ------- + + const renderCategoryItem = useCallback( + (item: MealCategory) => ( + handleOpenCategoryForm(item)} + > + + + + + {item.name} + + + { + e.stopPropagation(); + setCategoryDeleteConfirm(item); + }} + > + + + + + + ), + [handleOpenCategoryForm], + ); + + const renderChildTagItem = useCallback( + (item: MealTag) => ( + handleOpenTagForm(item.parent_id!, item)} + > + + + {item.name} + + + { + e.stopPropagation(); + setTagDeleteConfirm(item); + }} + > + + + + + + ), + [handleOpenTagForm], + ); + + const renderParentTag = useCallback( + (parent: MealTag) => { + const children = childTagsByParent.get(parent.id) ?? []; + const isExpanded = expandedParents.has(parent.id); + + return ( + + toggleParentExpanded(parent.id)} + > + + + + + {parent.name} + + + {children.length} 个子标签 + { + e.stopPropagation(); + handleOpenTagForm(parent.id); + }} + > + + + + + + + {isExpanded && ( + <> + {children.map(renderChildTagItem)} + handleOpenTagForm(parent.id)} + > + + 添加子标签 + + + )} + + ); + }, + [childTagsByParent, expandedParents, handleOpenTagForm, renderChildTagItem, toggleParentExpanded], + ); + + const renderSlotItem = useCallback( + (item: MealSlot, index: number) => { + const isActive = item.status === MealStatus.Enabled; + const isFirst = index === 0; + const isLast = index === slots.length - 1; + + return ( + handleOpenSlotForm(item)} + > + + {/* 排序按钮 */} + + { + e.stopPropagation(); + handleMoveSlot(index, "up"); + }} + > + + + { + e.stopPropagation(); + handleMoveSlot(index, "down"); + }} + > + + + + + {isActive ? ( + + ) : ( + + )} + + + + {item.name} + + + {isActive ? "已启用" : "已停用"} + + + + + { + e.stopPropagation(); + handleToggleSlotStatus(item); + }} + > + {isActive ? "停用" : "启用"} + + { + e.stopPropagation(); + setSlotDeleteConfirm(item); + }} + > + + + + + + ); + }, + [handleMoveSlot, handleOpenSlotForm, handleToggleSlotStatus, slots.length], + ); + + return ( + + + {/* ======== 分类管理 ======== */} + + 分类管理 + + + + + + + {categories.length > 0 ? ( + categories.map(renderCategoryItem) + ) : ( + + + + )} + handleOpenCategoryForm()} + > + + 添加分类 + + + + {/* ======== 标签管理 ======== */} + 标签管理 + + + + 父标签 + handleOpenTagForm(null)} + > + + 添加父标签 + + + + {parentTags.length > 0 ? ( + parentTags.map(renderParentTag) + ) : ( + + + + )} + + + {/* 无父标签的子标签(待归类标签) */} + {parentTags.length === 0 && ( + + )} + + {/* ======== 餐段管理 ======== */} + 餐段管理 + + + {slots.length > 0 ? ( + slots.map((slot, idx) => renderSlotItem(slot, idx)) + ) : ( + + + + )} + handleOpenSlotForm()} + > + + 添加餐段 + + + + + 餐段建议:早餐、午餐、晚餐、夜宵等 + + + + {/* ======== 分类 BottomSheet ======== */} + + + + + + {editingCategory ? "编辑分类" : "新增分类"} + + + + setCategoryForm({ name: v })} + placeholder="例如:家常菜" + /> + + + + + + + + + + {/* ======== 标签 BottomSheet ======== */} + + + + + + {editingTag ? "编辑标签" : editingParentId ? "新增子标签" : "新增父标签"} + + + + setTagForm((prev) => ({ ...prev, name: v }))} + placeholder="例如:微辣" + /> + + + + + {COLORS.map((c) => ( + setTagForm((prev) => ({ ...prev, color: c }))} + /> + ))} + + + setTagForm((prev) => ({ ...prev, color: "" }))} + > + 无颜色 + + setTagForm((prev) => ({ ...prev, color: v }))} + placeholder="#EF4444" + /> + + + + + + + + + + + {/* ======== 餐段 BottomSheet ======== */} + + + + + + {editingSlot ? "编辑餐段" : "新增餐段"} + + + + setSlotForm({ name: v })} + placeholder="例如:早餐" + /> + + + + + + + + + + {/* ======== 删除确认弹窗 ======== */} + {/* 分类 */} + { if (!v) setCategoryDeleteConfirm(null); }}> + + + + 删除分类 + + 确认删除分类「{categoryDeleteConfirm?.name}」吗?已归类的菜品不会受影响。 + + + + + + + + + + {/* 标签 */} + { if (!v) setTagDeleteConfirm(null); }}> + + + + 删除标签 + + {tagDeleteConfirm && !tagDeleteConfirm.parent_id + ? `确认删除父标签「${tagDeleteConfirm.name}」吗?其下的所有子标签也会被删除。` + : `确认删除标签「${tagDeleteConfirm?.name}」吗?`} + + + + + + + + + + {/* 餐段 */} + { if (!v) setSlotDeleteConfirm(null); }}> + + + + 删除餐段 + + 确认删除餐段「{slotDeleteConfirm?.name}」吗?历史餐食安排不受影响。 + + + + + + + + + + + + ); +} diff --git a/code/app/app-rn/src/app/meal/dish/detail/[id]/index.tsx b/code/app/app-rn/src/app/meal/dish/detail/[id]/index.tsx new file mode 100644 index 00000000..21eae6dc --- /dev/null +++ b/code/app/app-rn/src/app/meal/dish/detail/[id]/index.tsx @@ -0,0 +1,297 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Text, View } from "react-native"; +import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; +import { Button, Dialog, useThemeColor } from "heroui-native"; +import { PencilIcon, RotateCcwIcon, Trash2Icon } from "lucide-react-native"; +import PageLayout from "@/layouts/PageLayout"; +import LayoutView from "@/components/layout/view"; +import MealDishImage from "@/components/meal/MealDishImage"; +import EmptyState from "@/components/EmptyStatus"; +import { LoadingOverlay } from "@/components/LoadingOverlay"; +import { useLoading } from "@/hooks/useLoading"; +import { useFamilyStore, useMealStore } from "@/store"; +import { MealDishDifficulty, MealStatus } from "@/types/meal"; + +const DIFFICULTY_LABELS: Record = { + [MealDishDifficulty.None]: "", + [MealDishDifficulty.Easy]: "简单", + [MealDishDifficulty.Normal]: "普通", + [MealDishDifficulty.Hard]: "复杂", +}; + +export default function MealDishDetail() { + const router = useRouter(); + const { id } = useLocalSearchParams<{ id?: string }>(); + const dishId = id ? Number(id) : 0; + const familyId = useFamilyStore((s) => s.getFamilyId()); + const accentColor = useThemeColor("accent"); + const dishes = useMealStore((s) => s.dishes); + const categories = useMealStore((s) => s.categories); + const tags = useMealStore((s) => s.tags); + const dishTags = useMealStore((s) => s.dishTags); + const dishIngredients = useMealStore((s) => s.dishIngredients); + const dishSteps = useMealStore((s) => s.dishSteps); + const fetchDishes = useMealStore((s) => s.fetchDishes); + const fetchCategories = useMealStore((s) => s.fetchCategories); + const fetchTags = useMealStore((s) => s.fetchTags); + const fetchDishTags = useMealStore((s) => s.fetchDishTags); + const fetchDishIngredients = useMealStore((s) => s.fetchDishIngredients); + const fetchDishSteps = useMealStore((s) => s.fetchDishSteps); + const saveDishChanges = useMealStore((s) => s.saveDishChanges); + const { isLoading, startLoading, stopLoading } = useLoading(); + const [pageLoading, setPageLoading] = useState(true); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + + const item = useMemo(() => dishes.find((d) => d.id === dishId) ?? null, [dishId, dishes]); + + const categoryName = useMemo(() => { + if (!item?.category_id) return "未分类"; + return categories.find((c) => c.id === item.category_id)?.name ?? "未分类"; + }, [categories, item?.category_id]); + + const selectedTags = useMemo(() => { + const relationTagIds = new Set( + dishTags.filter((r) => r.dish_id === dishId).map((r) => r.tag_id), + ); + return tags.filter((t) => relationTagIds.has(t.id)); + }, [dishId, dishTags, tags]); + + const sortedSteps = useMemo( + () => [...dishSteps].sort((a, b) => a.step_number - b.step_number), + [dishSteps], + ); + + const loadData = useCallback(async () => { + if (!familyId || !dishId) return; + setPageLoading(true); + try { + await Promise.all([ + fetchDishes(familyId), + fetchCategories(familyId), + fetchTags(familyId), + fetchDishTags(familyId, dishId), + fetchDishIngredients(familyId, dishId), + fetchDishSteps(familyId, dishId), + ]); + } finally { + setPageLoading(false); + } + }, [familyId, dishId, fetchCategories, fetchDishIngredients, fetchDishSteps, fetchDishTags, fetchDishes, fetchTags]); + + useEffect(() => { + loadData(); + }, [loadData]); + + useFocusEffect( + useCallback(() => { + if (!familyId || !dishId) return; + loadData(); + }, [familyId, loadData, dishId]), + ); + + const handleDeleteConfirm = useCallback(async () => { + if (!familyId || !item || isLoading) return; + setShowDeleteDialog(false); + startLoading("停用中..."); + try { + const ok = await saveDishChanges({ + updates: [{ id: item.id, status: MealStatus.Disabled }], + }); + if (!ok) { + stopLoading({ type: "error", message: "停用失败" }); + return; + } + stopLoading({ type: "success", message: "已停用" }); + router.replace("/meal/dishes" as any); + } catch { + stopLoading({ type: "error", message: "停用失败" }); + } + }, [familyId, isLoading, item, router, saveDishChanges, startLoading, stopLoading]); + + return ( + + + + + {!item && !pageLoading ? ( + + ) : ( + item && ( + <> + + + + + + {item.name} + + + + {categoryName} + + {!!item.difficulty && item.difficulty > 0 && ( + + + {DIFFICULTY_LABELS[item.difficulty]} + + + )} + {!!item.cooking_time_minutes && item.cooking_time_minutes > 0 && ( + + 约{item.cooking_time_minutes}分钟 + + )} + + + + + + {/* 标签 */} + {selectedTags.length > 0 && ( + + 标签 + + {selectedTags.map((tag) => ( + + + {tag.name} + + + ))} + + + )} + + {/* 食材摘要 */} + {!!item.ingredient_summary && ( + + 主要食材 + {item.ingredient_summary} + + )} + + {/* 食材明细 */} + {dishIngredients.length > 0 && ( + + 食材明细 + + {dishIngredients.map((ing) => ( + + + {ing.name} + + {[ing.quantity ? `${ing.quantity}` : "", ing.unit, ing.spec] + .filter(Boolean) + .join(" ")} + + + ))} + + + )} + + {/* 做法步骤 */} + {sortedSteps.length > 0 && ( + + 做法步骤 + + {sortedSteps.map((step) => ( + + + {step.step_number} + + + {!!step.image_file_key && ( + + )} + {!!step.description && ( + {step.description} + )} + + + ))} + + + )} + + {/* 备注 */} + {!!item.remark && ( + + 备注 + {item.remark} + + )} + + + ) + )} + + + + {/* 底部操作栏 */} + + + + + + + {/* 停用确认 */} + + + + + 停用菜品 + + 确认停用「{item?.name}」吗?停用后菜品将从列表隐藏,历史餐食安排不受影响。 + + + + + + + + + + + + ); +} diff --git a/code/app/app-rn/src/app/meal/dish/edit/index.tsx b/code/app/app-rn/src/app/meal/dish/edit/index.tsx new file mode 100644 index 00000000..4d74a241 --- /dev/null +++ b/code/app/app-rn/src/app/meal/dish/edit/index.tsx @@ -0,0 +1,957 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import ImagePicker from "react-native-image-crop-picker"; +import { Button, Input, Label, TextArea, TextField } from "heroui-native"; +import { CheckIcon, ImageIcon, PlusIcon, SaveIcon, Trash2Icon, XIcon } from "lucide-react-native"; +import { toast } from "sonner-native"; +import PageLayout from "@/layouts/PageLayout"; +import LayoutView from "@/components/layout/view"; +import BottomSheet from "@/components/BottomSheet"; +import MealDishImage from "@/components/meal/MealDishImage"; +import { LoadingOverlay } from "@/components/LoadingOverlay"; +import { useLoading } from "@/hooks/useLoading"; +import { getUniqueIdApi } from "@/request/api"; +import { useFamilyStore, useMealStore } from "@/store"; +import { + MealCategory, + MealDish, + MealDishDifficulty, + MealDishIngredient, + MealDishStep, + MealDishTag, + MealStatus, + MealTag, +} from "@/types/meal"; + +// ------- 表单状态 ------- + +interface DishFormState { + name: string; + imageFileKey: string; + categoryId: number | null; + difficulty: MealDishDifficulty; + cookingTimeMinutes: number; + ingredientSummary: string; + remark: string; +} + +const emptyForm: DishFormState = { + name: "", + imageFileKey: "", + categoryId: null, + difficulty: MealDishDifficulty.None, + cookingTimeMinutes: 0, + ingredientSummary: "", + remark: "", +}; + +function createEditForm(item: MealDish): DishFormState { + return { + name: item.name, + imageFileKey: item.image_file_key, + categoryId: item.category_id ?? null, + difficulty: item.difficulty ?? MealDishDifficulty.None, + cookingTimeMinutes: item.cooking_time_minutes ?? 0, + ingredientSummary: item.ingredient_summary ?? "", + remark: item.remark ?? "", + }; +} + +const DIFFICULTY_OPTIONS = [ + { value: MealDishDifficulty.None, label: "不限" }, + { value: MealDishDifficulty.Easy, label: "简单" }, + { value: MealDishDifficulty.Normal, label: "普通" }, + { value: MealDishDifficulty.Hard, label: "复杂" }, +]; + +function toggleId(list: number[], id: number) { + return list.includes(id) ? list.filter((item) => item !== id) : [...list, id]; +} + +// ------- 页面 ------- + +export default function MealDishEdit() { + const router = useRouter(); + const { id } = useLocalSearchParams<{ id?: string }>(); + const editId = id ? Number(id) : 0; + const isEdit = editId > 0; + const familyId = useFamilyStore((s) => s.getFamilyId()); + const dishes = useMealStore((s) => s.dishes); + const categories = useMealStore((s) => s.categories); + const tags = useMealStore((s) => s.tags); + const dishTags = useMealStore((s) => s.dishTags); + const dishIngredients = useMealStore((s) => s.dishIngredients); + const dishSteps = useMealStore((s) => s.dishSteps); + const fetchDishes = useMealStore((s) => s.fetchDishes); + const fetchCategories = useMealStore((s) => s.fetchCategories); + const saveCategoryChanges = useMealStore((s) => s.saveCategoryChanges); + const saveTagChanges = useMealStore((s) => s.saveTagChanges); + const fetchTags = useMealStore((s) => s.fetchTags); + const fetchDishTags = useMealStore((s) => s.fetchDishTags); + const fetchDishIngredients = useMealStore((s) => s.fetchDishIngredients); + const fetchDishSteps = useMealStore((s) => s.fetchDishSteps); + const saveDishBundle = useMealStore((s) => s.saveDishBundle); + const uploadDishImage = useMealStore((s) => s.uploadDishImage); + const { isLoading, startLoading, stopLoading } = useLoading(); + + const [form, setForm] = useState(emptyForm); + const [selectedTagIds, setSelectedTagIds] = useState([]); + const [ingredients, setIngredients] = useState([]); + const [steps, setSteps] = useState([]); + const [uploading, setUploading] = useState(false); + const [pageLoading, setPageLoading] = useState(true); + + // 食材 BottomSheet + const [showIngredientSheet, setShowIngredientSheet] = useState(false); + const [ingredientForm, setIngredientForm] = useState>({}); + const [editingIngredientIndex, setEditingIngredientIndex] = useState(-1); + + // 步骤 BottomSheet + const [showStepSheet, setShowStepSheet] = useState(false); + const [stepForm, setStepForm] = useState>({}); + const [editingStepIndex, setEditingStepIndex] = useState(-1); + const [stepUploading, setStepUploading] = useState(false); + + const uploadStepImage = useMealStore((s) => s.uploadStepImage); + + // 快速新建分类 + const [showQuickCategorySheet, setShowQuickCategorySheet] = useState(false); + const [quickCategoryName, setQuickCategoryName] = useState(""); + + // 快速新建标签 + const [showQuickTagSheet, setShowQuickTagSheet] = useState(false); + const [quickTagName, setQuickTagName] = useState(""); + const [quickTagColor, setQuickTagColor] = useState(""); + const [quickTagParentId, setQuickTagParentId] = useState(null); + + // 标签按父标签分组 + const parentTags = useMemo(() => tags.filter((t) => !t.parent_id), [tags]); + const childTagsByParent = useMemo(() => { + const map = new Map(); + tags + .filter((t) => !!t.parent_id) + .forEach((tag) => { + const group = map.get(tag.parent_id!) ?? []; + group.push(tag); + map.set(tag.parent_id!, group); + }); + return map; + }, [tags]); + + const currentItem = useMemo( + () => dishes.find((item) => item.id === editId) ?? null, + [dishes, editId], + ); + + // ------- 数据加载 ------- + + const loadBaseData = useCallback(async () => { + if (!familyId) return; + setPageLoading(true); + try { + await Promise.all([ + fetchDishes(familyId), + fetchCategories(familyId), + fetchTags(familyId), + fetchDishTags(familyId, isEdit ? editId : undefined), + ]); + + if (isEdit) { + const state = useMealStore.getState(); + const item = state.dishes.find((row) => row.id === editId); + if (!item) { + toast.error("菜品不存在"); + router.back(); + return; + } + setForm(createEditForm(item)); + setSelectedTagIds( + state.dishTags.filter((row) => row.dish_id === editId).map((row) => row.tag_id), + ); + + await Promise.all([ + fetchDishIngredients(familyId, editId), + fetchDishSteps(familyId, editId), + ]); + const s = useMealStore.getState(); + setIngredients(s.dishIngredients); + setSteps(s.dishSteps); + } + } finally { + setPageLoading(false); + } + }, [editId, familyId, fetchCategories, fetchDishes, fetchDishIngredients, fetchDishSteps, fetchDishTags, fetchTags, isEdit, router]); + + useEffect(() => { + loadBaseData(); + }, [loadBaseData]); + + // ------- 图片选择 ------- + + const handlePickImage = useCallback(async () => { + if (!familyId || uploading) return; + let toastId: string | number | undefined; + try { + setUploading(true); + toastId = toast.loading("图片上传中"); + const img = await ImagePicker.openPicker({ + cropping: true, + width: 800, + height: 600, + compressImageQuality: 0.86, + mediaType: "photo", + }); + const file = { + uri: img.path, + name: img.filename || `dish_${Date.now()}.jpg`, + type: img.mime || "image/jpeg", + }; + const key = await uploadDishImage(familyId, file); + if (!key) { + toast.error("图片上传失败", { id: toastId }); + return; + } + setForm((prev) => ({ ...prev, imageFileKey: key })); + toast.success("图片已上传", { id: toastId }); + } catch (error: any) { + if (error?.code === "E_PICKER_CANCELLED") { + if (toastId) toast.dismiss(toastId); + } else if (toastId) { + toast.error("图片选择失败", { id: toastId }); + } + } finally { + setUploading(false); + } + }, [familyId, uploadDishImage, uploading]); + + const handlePickStepImage = useCallback(async () => { + if (!familyId || stepUploading) return; + try { + setStepUploading(true); + const img = await ImagePicker.openPicker({ + cropping: true, + width: 800, + height: 600, + compressImageQuality: 0.86, + mediaType: "photo", + }); + const file = { + uri: img.path, + name: img.filename || `step_${Date.now()}.jpg`, + type: img.mime || "image/jpeg", + }; + const key = await uploadStepImage(familyId, file); + if (key) { + setStepForm((prev) => ({ ...prev, image_file_key: key })); + } + } catch { + // cancelled + } finally { + setStepUploading(false); + } + }, [familyId, uploadStepImage, stepUploading]); + + // ------- 食材管理 ------- + + const handleAddIngredient = useCallback(() => { + setEditingIngredientIndex(-1); + setIngredientForm({ name: "", quantity: undefined, unit: "", spec: "", remark: "" }); + setShowIngredientSheet(true); + }, []); + + const handleEditIngredient = useCallback((index: number) => { + setEditingIngredientIndex(index); + setIngredientForm(ingredients[index]); + setShowIngredientSheet(true); + }, [ingredients]); + + const handleSaveIngredient = useCallback(() => { + const name = (ingredientForm.name ?? "").trim(); + if (!name) { + toast.error("请输入食材名称"); + return; + } + const item: MealDishIngredient = { + id: ingredientForm.id ?? 0, + family_id: familyId, + dish_id: editId, + name, + quantity: ingredientForm.quantity, + unit: ingredientForm.unit ?? "", + spec: ingredientForm.spec ?? "", + remark: ingredientForm.remark ?? "", + sort_number: editingIngredientIndex >= 0 ? ingredients[editingIngredientIndex].sort_number : ingredients.length, + }; + + if (editingIngredientIndex >= 0) { + const updated = [...ingredients]; + updated[editingIngredientIndex] = item; + setIngredients(updated); + } else { + setIngredients([...ingredients, item]); + } + setShowIngredientSheet(false); + }, [editId, editingIngredientIndex, familyId, ingredientForm, ingredients]); + + const handleRemoveIngredient = useCallback((index: number) => { + setIngredients(ingredients.filter((_, i) => i !== index)); + }, [ingredients]); + + // ------- 步骤管理 ------- + + const handleAddStep = useCallback(() => { + setEditingStepIndex(-1); + setStepForm({ step_number: steps.length + 1, description: "", image_file_key: "" }); + setShowStepSheet(true); + }, [steps.length]); + + const handleEditStep = useCallback((index: number) => { + setEditingStepIndex(index); + setStepForm(steps[index]); + setShowStepSheet(true); + }, [steps]); + + const handleSaveStep = useCallback(() => { + const desc = (stepForm.description ?? "").trim(); + const imgKey = stepForm.image_file_key ?? ""; + if (!desc && !imgKey) { + toast.error("步骤描述或图片至少填写一项"); + return; + } + const item: MealDishStep = { + id: stepForm.id ?? 0, + family_id: familyId, + dish_id: editId, + step_number: editingStepIndex >= 0 ? steps[editingStepIndex].step_number : steps.length + 1, + description: desc, + image_file_key: imgKey, + sort_number: editingStepIndex >= 0 ? steps[editingStepIndex].sort_number : steps.length, + }; + + if (editingStepIndex >= 0) { + const updated = [...steps]; + updated[editingStepIndex] = item; + setSteps(updated); + } else { + setSteps([...steps, item]); + } + setShowStepSheet(false); + }, [editId, editingStepIndex, familyId, stepForm, steps]); + + const handleRemoveStep = useCallback((index: number) => { + setSteps(steps.filter((_, i) => i !== index)); + }, [steps]); + + // ------- 快速新建分类 ------- + + const handleQuickCreateCategory = useCallback(async () => { + if (!familyId || !quickCategoryName.trim()) return; + const id = (await getUniqueIdApi(1)).data?.[0]; + if (!id) { + toast.error("创建分类失败"); + return; + } + const ok = await saveCategoryChanges({ + inserts: [{ id, family_id: familyId, name: quickCategoryName.trim(), status: MealStatus.Enabled }], + }); + if (ok) { + await fetchCategories(familyId); + const state = useMealStore.getState(); + const created = state.categories.find((c) => c.name === quickCategoryName.trim()); + if (created) { + setForm((prev) => ({ ...prev, categoryId: created.id })); + } + toast.success("分类已创建"); + } + setQuickCategoryName(""); + setShowQuickCategorySheet(false); + }, [familyId, fetchCategories, quickCategoryName, saveCategoryChanges]); + + // ------- 快速新建标签 ------- + + const handleQuickCreateTag = useCallback(async () => { + if (!familyId || !quickTagName.trim()) return; + const id = (await getUniqueIdApi(1)).data?.[0]; + if (!id) { + toast.error("创建标签失败"); + return; + } + const ok = await saveTagChanges({ + inserts: [{ id, family_id: familyId, name: quickTagName.trim(), color: quickTagColor, parent_id: quickTagParentId, status: MealStatus.Enabled }], + }); + if (ok) { + await fetchTags(familyId); + const state = useMealStore.getState(); + const created = state.tags.find((t) => t.name === quickTagName.trim() && t.parent_id === quickTagParentId); + if (created) { + setSelectedTagIds((prev) => [...prev, created.id]); + } + toast.success("标签已创建"); + } + setQuickTagName(""); + setQuickTagColor(""); + setQuickTagParentId(null); + setShowQuickTagSheet(false); + }, [familyId, fetchTags, quickTagColor, quickTagName, quickTagParentId, saveTagChanges]); + + // ------- 保存 ------- + + const handleSave = useCallback(async () => { + const name = form.name.trim(); + if (!name) { + toast.error("请输入菜名"); + return; + } + if (!form.imageFileKey) { + toast.error("请上传菜品图片"); + return; + } + if (!familyId || isLoading) return; + + startLoading("保存中..."); + try { + const targetId = isEdit ? editId : (await getUniqueIdApi(1)).data?.[0]; + if (!targetId) { + stopLoading({ type: "error", message: "获取菜品 ID 失败" }); + return; + } + + const currentTagIds = new Set(dishTags.filter((r) => r.dish_id === targetId).map((r) => r.tag_id)); + const tagDeletes = dishTags + .filter((r) => r.dish_id === targetId && !selectedTagIds.includes(r.tag_id)) + .map((r) => ({ id: r.id })); + const newTagIds = selectedTagIds.filter((tid) => !currentTagIds.has(tid)); + const tagIdList = newTagIds.length ? ((await getUniqueIdApi(newTagIds.length)).data ?? []) : []; + if (tagIdList.length !== newTagIds.length) { + stopLoading({ type: "error", message: "获取标签关联 ID 失败" }); + return; + } + + // 生成食材 ID + const newIngredients = ingredients.filter((ing) => !ing.id); + const ingIdList = newIngredients.length ? ((await getUniqueIdApi(newIngredients.length)).data ?? []) : []; + + // 生成步骤 ID + const newSteps = steps.filter((s) => !s.id); + const stepIdList = newSteps.length ? ((await getUniqueIdApi(newSteps.length)).data ?? []) : []; + + const row = { + id: targetId, + family_id: familyId, + name, + image_file_key: form.imageFileKey, + category_id: form.categoryId, + ingredient_summary: form.ingredientSummary.trim(), + cooking_time_minutes: form.cookingTimeMinutes, + difficulty: form.difficulty, + remark: form.remark.trim(), + sort_number: dishes.length, + status: MealStatus.Enabled, + }; + + const ok = await saveDishBundle({ + dishInserts: isEdit ? [] : [row], + dishUpdates: isEdit ? [row] : [], + dishTagInserts: newTagIds.map((tagId, i) => ({ + id: tagIdList[i], + family_id: familyId, + dish_id: targetId, + tag_id: tagId, + })), + dishTagDeletes: tagDeletes, + dishIngredientInserts: newIngredients.map((ing, i) => ({ + ...ing, + id: ingIdList[i], + dish_id: targetId, + family_id: familyId, + })), + dishIngredientUpdates: ingredients.filter((ing) => !!ing.id).map((ing) => ({ + ...ing, + dish_id: targetId, + family_id: familyId, + })), + dishIngredientDeletes: [], // 全量替换模型 + dishStepInserts: newSteps.map((s, i) => ({ + ...s, + id: stepIdList[i], + dish_id: targetId, + family_id: familyId, + })), + dishStepUpdates: steps.filter((s) => !!s.id).map((s) => ({ + ...s, + dish_id: targetId, + family_id: familyId, + })), + dishStepDeletes: [], + }); + + if (!ok) { + stopLoading({ type: "error", message: "保存失败" }); + return; + } + + await Promise.all([fetchDishes(familyId), fetchDishTags(familyId)]); + stopLoading({ type: "success", message: isEdit ? "菜品已保存" : "菜品已新增" }); + router.back(); + } catch { + stopLoading({ type: "error", message: "保存失败" }); + } + }, [ + dishes.length, dishTags, editId, familyId, fetchDishes, fetchDishTags, form, + ingredients, isEdit, isLoading, router, saveDishBundle, selectedTagIds, + startLoading, steps, stopLoading, + ]); + + // ------- 渲染辅助 ------- + + const renderCategoryChip = useCallback( + (item: MealCategory) => { + const selected = form.categoryId === item.id; + return ( + + setForm((prev) => ({ ...prev, categoryId: prev.categoryId === item.id ? null : item.id })) + } + > + {item.name} + + ); + }, + [form.categoryId], + ); + + const renderTagChip = useCallback( + (item: MealTag) => { + const selected = selectedTagIds.includes(item.id); + return ( + setSelectedTagIds((prev) => toggleId(prev, item.id))} + > + {item.name} + + ); + }, + [selectedTagIds], + ); + + return ( + + + + + {/* 图片 */} + + {form.imageFileKey ? ( + + ) : ( + + + 上传菜品图片 + + )} + + + {/* 基本信息 */} + + + + setForm((prev) => ({ ...prev, name: v }))} + placeholder="例如:番茄炒蛋" + /> + + + + + + setForm((prev) => ({ ...prev, cookingTimeMinutes: parseInt(v) || 0 })) + } + placeholder="例如:15" + keyboardType="numeric" + /> + + + + + setForm((prev) => ({ ...prev, ingredientSummary: v }))} + placeholder="例如:番茄、鸡蛋、葱" + /> + + + + +