From 3dbc6f0ff5cfb3caca05dae2d93153063a780528 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Fri, 10 Apr 2026 02:17:34 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=A0=87=E8=AE=B0=E7=82=B9?= =?UTF-8?q?=E5=BA=8F=E5=8F=B7=E6=98=BE=E7=A4=BA=E3=80=81=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E5=8F=A0=E5=8A=A0=E6=96=B9=E5=BC=8F=E5=8F=8A=E7=85=A7=E7=89=87?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 标记点角标从物品数量改为按楼层顺序编号,角标背景色跟随标记颜色 - FoldableLayout 改为绝对定位叠加布局,移除 LayoutAnimation(解决 Android 地图渲染空白及卡顿) - PhotoPicker 拆分为拍照/选图两个独立按钮;选图改用 expo-document-picker 绕过三星 Photo Picker 崩溃问题 - AndroidManifest 移除手动 FileProvider 声明(改由 Expo 插件自动注入) - 同步更新蓝图文档:新增 category-management 模块、补全各模块已完成任务卡 Co-Authored-By: Claude Sonnet 4.6 --- .blueprint/CONVENTIONS.md | 4 +- .blueprint/manifest.yaml | 17 ++++- .blueprint/modules/category-management.md | 31 ++++++++ .blueprint/modules/floor-map.md | 40 +++++++---- .blueprint/modules/item-form.md | 23 +++--- .blueprint/modules/local-data-layer.md | 29 +++++--- .blueprint/modules/photo-capture.md | 32 +++++++-- .blueprint/modules/shared-types.md | 6 +- .blueprint/modules/survey-app-shell.md | 30 ++++---- CLAUDE.md | 42 +++++++---- apps/property-survey/.expo/devices.json | 2 +- .../android/app/src/main/AndroidManifest.xml | 11 +-- apps/property-survey/package.json | 1 + .../src/features/map/FloorMap.tsx | 6 +- .../src/features/map/MapMarker.tsx | 21 +++--- .../src/features/map/MarkerList.tsx | 12 ++-- .../src/features/photo/PhotoPicker.tsx | 31 ++++---- .../src/features/photo/usePhotoCapture.ts | 72 +++++++++---------- .../src/layout/FoldableLayout.tsx | 28 +++----- pnpm-lock.yaml | 12 ++++ 20 files changed, 276 insertions(+), 174 deletions(-) create mode 100644 .blueprint/modules/category-management.md diff --git a/.blueprint/CONVENTIONS.md b/.blueprint/CONVENTIONS.md index c23caaa..df8735a 100644 --- a/.blueprint/CONVENTIONS.md +++ b/.blueprint/CONVENTIONS.md @@ -1,6 +1,6 @@ --- -version: 1.4.2 -updated: 2026-04-08 +version: 1.4.3 +updated: 2026-04-10 source: dev-manager-tauri --- diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index 30f3f5a..ddc60ac 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -1,7 +1,7 @@ version: 1 name: react-phone-apps iteration: 2 -updated: 2026-04-09 +updated: 2026-04-10 areas: @@ -103,6 +103,13 @@ modules: progress: 100 position: [980, 880] + - id: category-management + name: 物品分类管理 + area: rn-app + status: done + progress: 100 + position: [980, 1080] + # ── 前端应用(Web,预留)────────────────────────────────────── - id: app-scaffold name: 应用脚手架模板 @@ -202,6 +209,14 @@ edges: to: photo-capture type: dependency + # 分类管理 + - from: local-data-layer + to: category-management + type: dependency + - from: category-management + to: item-form + type: dependency + # Web 前端(预留) - from: monorepo-setup to: app-scaffold diff --git a/.blueprint/modules/category-management.md b/.blueprint/modules/category-management.md new file mode 100644 index 0000000..905b862 --- /dev/null +++ b/.blueprint/modules/category-management.md @@ -0,0 +1,31 @@ +--- +id: category-management +name: 物品分类管理 +area: rn-app +status: done +--- + +## 概述 + +独立的「设置」Tab,允许用户自定义物品分类标签(增删改)。分类数据存储在 SQLite categories 表,通过全局 CategoriesContext 供物品表单实时使用,替代原有硬编码枚举。 + +## 任务卡 + +### ✅ 分类数据库表与 CRUD +- status: done +- complexity: S +- files: apps/property-survey/src/db/categories.ts, apps/property-survey/src/db/schema.ts, apps/property-survey/src/db/database.ts +- acceptance: categories 表含 id / name / sort_order 三列;提供 getCategories / insertCategory / updateCategory / deleteCategory 四个函数;App 首次启动时写入「家具、家电、电子、车辆、其他」五个默认分类 + +### ✅ 全局分类 Context +- status: done +- complexity: S +- files: apps/property-survey/src/features/settings/CategoriesContext.tsx, apps/property-survey/app/_layout.tsx +- acceptance: CategoriesProvider 在根布局包裹全树;useCategories() 返回 `{ categories, reload }`;categories 在分类增删改后自动刷新,物品表单的分类 chip 同步更新 + +### ✅ 设置页 UI(分类增删改) +- status: done +- complexity: M +- files: apps/property-survey/app/settings.tsx +- depends: category-management +- acceptance: 底部 Tab 新增「设置」入口;列表展示当前所有分类,支持点击铅笔图标内联重命名(TextInput + 确认/取消);底部固定新增输入框 + 按钮;删除时弹确认框,说明已登记物品的分类标签不受影响 diff --git a/.blueprint/modules/floor-map.md b/.blueprint/modules/floor-map.md index 9545d7f..42725ca 100644 --- a/.blueprint/modules/floor-map.md +++ b/.blueprint/modules/floor-map.md @@ -7,34 +7,50 @@ status: done ## 概述 -以 SVG/Image 占位图为底图,支持长按添加标记点、点击标记点进入编辑的交互逻辑。标记点坐标以相对比例(0~1)存储,替换真实楼层图时无需修改数据。 +以真实 SVG 楼层平面图为底图,支持捏合缩放、单指平移、长按添加标记点、拖拽移动标记点。支持 1F/2F/3F/楼顶四个楼层。SVG 内嵌图片通过构建脚本提取,分层以原生 Image 渲染,保证清晰度和旋转精度。 ## 任务卡 -### 🔵 平面图占位容器与坐标系 +### ✅ SVG 地图渲染(多层架构) +- status: done +- complexity: M +- files: apps/property-survey/src/features/map/FloorMap.tsx, apps/property-survey/src/features/map/floorMaps.generated.ts, apps/property-survey/scripts/generate-maps.js +- acceptance: 三层渲染:① 底图 JPEG(原生 Image,from imageUri)② SVG 矢量路径(SvgXml,overdraw 预渲染提升缩放清晰度,动态计算安全 overdraw 倍数避免 Android Canvas OOM)③ 用户嵌入图片(原生 Image,从 SVG 提取并按 SVG matrix/rotate 变换还原坐标和旋转角) + +### ✅ 捏合缩放 + 单指平移手势 - status: done - complexity: M - files: apps/property-survey/src/features/map/FloorMap.tsx -- depends: survey-app-shell -- acceptance: 渲染一个带灰色背景的矩形占位区域作为平面图,使用 onLayout 获取实际像素尺寸并转换相对坐标(0~1),容器宽高比固定为 4:3,适配折叠/展开两种宽度 +- acceptance: 使用 react-native-gesture-handler Gesture API;Pinch 手势控制缩放(MIN_SCALE=1, MAX_SCALE=5);Pan 手势(minPointers:1, maxPointers:1)控制平移;双指和单指通过 Gesture.Race 互斥;使用 react-native-reanimated SharedValue 实现原生线程动画 -### 🔵 长按手势添加标记点 +### ✅ 长按添加标记点 - status: done - complexity: M -- files: apps/property-survey/src/features/map/useMarkerGesture.ts -- depends: floor-map -- acceptance: 在平面图区域长按 500ms 后触发,将触点位置转换为相对坐标并调用 insertMarker 写入数据库,新标记点立即在地图上显示,不误触短按事件 +- files: apps/property-survey/src/features/map/FloorMap.tsx +- acceptance: LongPress 手势(minDuration:500ms);触点坐标反算缩放/平移变换得到地图相对坐标(0~1);调用 insertMarker 写入数据库;与双指手势通过 Gesture.Race 互斥 -### 🔵 标记点图标组件 +### ✅ 标记点图标组件 - status: done - complexity: M - files: apps/property-survey/src/features/map/MapMarker.tsx - depends: floor-map -- acceptance: 在平面图上按相对坐标绝对定位渲染图钉图标,已有物品的标记点显示蓝色图钉 + 物品数量角标,空标记点显示灰色图钉,点击触发 onPress 回调 +- acceptance: 按相对坐标绝对定位;已有物品的标记点显示彩色图钉 + 数量角标(颜色来自 marker.color);选中状态有缩放高亮;支持拖拽移动(Pan 手势,拖拽结束调用 onDragEnd) -### 🔵 当前楼层标记点数据加载 +### ✅ 楼层标记点数据加载与标记列表 - status: done - complexity: S - files: apps/property-survey/src/features/map/MarkerList.tsx - depends: floor-map -- acceptance: 楼层切换时调用 getMarkersByFloor 重新查询,渲染该楼层所有 MapMarker,删除标记点时弹出确认对话框(含级联删除提示) +- acceptance: 楼层切换或 refreshKey 变化时重新查询;左侧边栏展示当前楼层所有标记点卡片(标签、物品列表含确认状态图标);删除标记点时弹确认对话框含级联删除提示 + +### ✅ SVG 地图生成脚本 +- status: done +- complexity: M +- files: scripts/generate-maps.js, apps/property-survey/img/(1101-1104.svg) +- acceptance: 读取 4 个 SVG 文件;提取背景 JPEG(imageUri)+ 用户嵌入图片(embeddedImages,含 cx/cy/width/height/angleDeg/opacity);从 svgXml 中去除所有 image 元素及 Inkscape 私有命名空间;生成 floorMaps.generated.ts TypeScript 文件 + +## 决策记录 + +- **SVG 三层渲染架构**:`SvgXml` 无法可靠渲染带 `rotate`/`matrix` transform 的 `` 元素(坐标偏移、旋转不准)。改为构建时提取:① 背景 JPEG 用原生 `Image` 渲染(性能好);② 矢量路径用 `SvgXml` 渲染(保留矢量清晰度);③ 用户嵌入图片用原生 `Image` 渲染(精确还原旋转和位置)。 +- **SVG overdraw 预渲染**:`SvgXml` 在 RN 中被光栅化为固定分辨率纹理,缩放时模糊。通过以 N 倍宽高渲染再 `scale(1/N)` 缩回,在 Android Canvas 安全内存上限内最大化预渲染分辨率。N 值由 `calcSafeOverdraw()` 根据 `PixelRatio.get()` 动态计算,避免高 DPI 设备 OOM。 +- **嵌入图片坐标还原**:SVG `matrix(a,b,c,d,e,f)` 变换的图片中心点计算公式:`cx = a*(ox+W/2) + c*(oy+H/2) + e`,旋转角 `angleDeg = atan2(b,a) * 180/π`。RN 中 Image 的 `transform: rotate` 以元素自身中心为锚点,与 SVG transform 语义一致,无需额外偏移修正。 diff --git a/.blueprint/modules/item-form.md b/.blueprint/modules/item-form.md index 012aa16..9a2120d 100644 --- a/.blueprint/modules/item-form.md +++ b/.blueprint/modules/item-form.md @@ -7,27 +7,34 @@ status: done ## 概述 -点击地图标记点后弹出的物品编辑面板。折叠时以底部抽屉形式展现,展开时在右侧栏内联显示。支持单个标记点下的多条物品记录增删改。 +点击地图标记点后弹出的物品编辑面板(右侧栏内联)。支持单个标记点下的多条物品记录增删改,物品字段含名称、动态分类、数量、单位、规格、单价、参考价、备注,以及确认状态标记。 ## 任务卡 -### 🔵 底部抽屉/右栏容器 +### ✅ 底部抽屉/右栏容器 - status: done - complexity: M - files: apps/property-survey/src/features/item/ItemFormSheet.tsx - depends: survey-app-shell, floor-map -- acceptance: 折叠时从屏幕底部滑出(BottomSheet),展开时在 FoldableLayout 右栏内渲染;接受 markerId 和 onClose 两个 prop;顶部显示标记点位置名称和所属楼层 +- acceptance: 右栏内渲染;顶部显示标记点标签 + 楼层 + 颜色选择器;头部操作区含复制标记点、删除标记点、关闭按钮 -### 🔵 物品字段表单 +### ✅ 物品字段表单 - status: done - complexity: M - files: apps/property-survey/src/features/item/ItemFields.tsx, apps/property-survey/src/features/item/useItemForm.ts -- depends: item-form -- acceptance: 包含物品名称(必填文本)、数量(数字键盘)、单价(数字键盘,可选)、备注1(多行文本)、备注2(多行文本)五个字段;保存时校验物品名称非空;useItemForm 管理字段状态和 insertItem / updateItem 调用 +- depends: item-form, category-management +- acceptance: 字段:物品名称(必填)、动态分类 chip(来自 CategoriesContext)、数量、单位、单价、参考价、规格(多行)、备注(多行);底部固定保存按钮 + 确认状态勾选框(显示确认时间或「待确认」);保存时校验物品名称非空;保存成功后显示 Toast 提示(「已保存」) -### 🔵 标记点下物品列表 +### ✅ 标记点下物品列表 - status: done - complexity: M - files: apps/property-survey/src/features/item/ItemList.tsx - depends: item-form -- acceptance: 在表单上方列出当前标记点的所有物品(物品名 + 数量 + 单价),点击条目进入编辑,左滑显示删除按钮,点击「+新增物品」清空表单进入新增模式 +- acceptance: 横向滚动列表展示当前标记点所有物品;点击条目进入编辑;含新增物品入口;物品卡片显示名称、数量、照片数 + +### ✅ 物品确认状态 +- status: done +- complexity: S +- files: apps/property-survey/src/features/item/ItemFields.tsx, apps/property-survey/src/features/item/useItemForm.ts, apps/property-survey/src/db/items.ts +- depends: item-form +- acceptance: 表单底部显示勾选框;未确认显示「待确认」灰色文字,已确认显示绿色确认时间(MM-DD HH:mm);点击切换状态,保存时写入 confirmed_at 字段;标记点列表侧边栏同步显示每个物品的确认状态图标 diff --git a/.blueprint/modules/local-data-layer.md b/.blueprint/modules/local-data-layer.md index a960f3b..9824e59 100644 --- a/.blueprint/modules/local-data-layer.md +++ b/.blueprint/modules/local-data-layer.md @@ -7,33 +7,46 @@ status: done ## 概述 -基于 Expo SQLite 的本地数据库层,管理标记点、物品记录和照片路径三张核心表。所有写操作使用事务保证原子性,供上层功能模块调用。 +基于 Expo SQLite 的本地数据库层,管理标记点、物品记录、照片路径、物品分类四张核心表。所有写操作使用事务保证原子性,供上层功能模块调用。支持在线迁移:每次启动检测旧库结构并补列/重建表,无需用户操作。 ## 任务卡 -### 🔵 SQLite 初始化与 Schema 建表 +### ✅ SQLite 初始化与 Schema 建表 - status: done - complexity: M - files: apps/property-survey/src/db/database.ts, apps/property-survey/src/db/schema.ts -- acceptance: App 启动时自动执行 CREATE TABLE IF NOT EXISTS,建立 markers / items / photos 三张表,重复启动不报错,字段与设计文档一致 +- acceptance: App 启动时自动执行 CREATE TABLE IF NOT EXISTS,建立 markers / items / photos / categories 四张表,重复启动不报错;首次启动写入默认分类;字段与设计文档一致 -### 🔵 标记点 CRUD 操作 +### ✅ 在线迁移脚本 +- status: done +- complexity: M +- files: apps/property-survey/src/db/database.ts +- acceptance: 启动时检测旧库并执行:①为 items 添加 reference_price / spec / unit / confirmed_at 列;②重建 markers 表以扩展 floor CHECK 约束支持第4楼(楼顶);③为旧库初始化 categories 默认数据;所有迁移幂等,多次执行不报错 + +### ✅ 标记点 CRUD 操作 - status: done - complexity: M - files: apps/property-survey/src/db/markers.ts - depends: local-data-layer -- acceptance: 提供 insertMarker / getMarkersByFloor / updateMarker / deleteMarker 四个函数,deleteMarker 级联删除关联 items 和 photos,所有函数返回类型安全的 Promise +- acceptance: 提供 insertMarker / getMarkersByFloor / updateMarker / deleteMarker 四个函数,deleteMarker 级联删除关联 items 和 photos,所有函数返回类型安全的 Promise;floor 支持 1/2/3/4 -### 🔵 物品记录 CRUD 操作 +### ✅ 物品记录 CRUD 操作 - status: done - complexity: M - files: apps/property-survey/src/db/items.ts - depends: local-data-layer -- acceptance: 提供 insertItem / getItemsByMarker / updateItem / deleteItem 四个函数,支持批量查询(传入 marker_id 数组),返回带关联照片数量的扩展类型 +- acceptance: 提供 insertItem / getItemsByMarker / updateItem / deleteItem 四个函数;字段含 name / category / quantity / unit / spec / estimatedValue / referencePrice / remark / confirmedAt;返回带关联照片数量的扩展类型 -### 🔵 照片路径 CRUD 操作 +### ✅ 照片路径 CRUD 操作 - status: done - complexity: S - files: apps/property-survey/src/db/photos.ts - depends: local-data-layer - acceptance: 提供 insertPhoto / getPhotosByItem / deletePhoto / reorderPhotos 四个函数,deletePhoto 同步删除设备文件系统中的对应图片文件 + +### ✅ 分类 CRUD 操作 +- status: done +- complexity: S +- files: apps/property-survey/src/db/categories.ts +- depends: local-data-layer +- acceptance: 提供 getCategories / insertCategory / updateCategory / deleteCategory 四个函数;新增时自动计算 sort_order(追加到末尾);name 字段有 UNIQUE 约束 diff --git a/.blueprint/modules/photo-capture.md b/.blueprint/modules/photo-capture.md index d98d1ca..8ceb8f2 100644 --- a/.blueprint/modules/photo-capture.md +++ b/.blueprint/modules/photo-capture.md @@ -7,20 +7,38 @@ status: done ## 概述 -调用设备摄像头或相册为物品条目添加多张照片,照片压缩后存储到 App 专属目录,路径写入 SQLite photos 表。 +调用设备摄像头或相册为物品条目添加多张照片,照片存储到 App 专属目录,路径写入 SQLite photos 表。支持 Android 13+ 权限模型(CAMERA + READ_MEDIA_IMAGES + FileProvider)。 ## 任务卡 -### 🔵 摄像头调用与图片压缩 +### ✅ 摄像头调用与照片存储 - status: done - complexity: M -- files: apps/property-survey/src/features/photo/usePhotoCapture.ts +- files: apps/property-survey/src/features/photo/usePhotoCapture.ts, apps/property-survey/src/features/photo/PhotoPicker.tsx - depends: local-data-layer -- acceptance: 使用 expo-image-picker 提供「拍照」和「从相册选择」两种入口;拍摄/选择后自动压缩至最大宽度 1080px、JPEG quality 0.75;将压缩后的文件复制到 expo-file-system 的 App 专属目录;调用 insertPhoto 写入数据库;全流程有权限检查,拒绝权限时给出提示 +- acceptance: 使用 expo-image-picker(MediaType.Images,已从废弃的 MediaTypeOptions 迁移);「拍照」和「从相册选择」两种入口;Alert 关闭后延迟 300ms 启动,避免与系统权限弹窗冲突;照片复制到 App 专属目录(expo-file-system);调用 insertPhoto 写入数据库;有权限检查 -### 🔵 照片网格预览与删除 +### ✅ Android 权限与 FileProvider 配置 +- status: done +- complexity: S +- files: apps/property-survey/android/app/src/main/AndroidManifest.xml, apps/property-survey/android/app/src/main/res/xml/file_paths.xml, apps/property-survey/app.json +- acceptance: AndroidManifest 声明 CAMERA + READ_MEDIA_IMAGES 权限(兼容 Android 13+);配置 FileProvider(android:authorities="${applicationId}.provider")支持相机临时文件 URI;app.json 中 expo-image-picker 插件配置中文权限说明 + +### ✅ 照片网格预览与删除(物品表单内) - status: done - complexity: M -- files: apps/property-survey/src/features/photo/PhotoGrid.tsx, apps/property-survey/src/features/photo/PhotoPicker.tsx +- files: apps/property-survey/src/features/photo/PhotoGrid.tsx - depends: photo-capture -- acceptance: 以 3 列网格展示已添加的照片缩略图;点击照片全屏预览(支持左右滑动切换);长按照片显示「删除」确认弹窗,删除时同步调用 deletePhoto(含文件系统删除);末尾显示「+」按钮触发 PhotoPicker 添加更多 +- acceptance: 3 列网格展示已添加的照片缩略图;点击照片全屏预览(FlatList + pagingEnabled + getItemLayout,使用 useWindowDimensions 实时宽度计算每页偏移);全屏预览顶部有「保存到相册」按钮(expo-media-library,保存后显示"已保存"提示);长按照片显示「删除」确认弹窗,删除时同步调用 deletePhoto(含文件系统删除);末尾显示「+」按钮触发 PhotoPicker + +### ✅ 查看模式照片面板(MarkerPhotoPanel) +- status: done +- complexity: M +- files: apps/property-survey/src/features/photo/MarkerPhotoPanel.tsx +- depends: photo-capture +- acceptance: 查看模式下右侧面板;按物品分组展示照片缩略图(4 列网格 + 物品名标题);点击缩略图全屏 Modal 预览;Modal 为非透明全屏黑底(解决 Android transparent Modal 仅覆盖父容器的问题);单张显示 + onTouchStart/End 手势滑动切换(≥50px 触发)+ 左右箭头按钮;计数器实时同步 + +## 决策记录 + +- **照片预览放弃 ScrollView/FlatList pagingEnabled**:在 Android 上 `transparent` Modal 的渲染范围被限制在父组件(右侧 260px 面板)内而非全屏,导致 `pagingEnabled` 页宽与实际视口不匹配,两张照片始终并排显示。尝试多种宽度测量方案(`SCREEN_W` 静态值、`onLayout` 动态测量、`useWindowDimensions`)均无效。最终改为单张显示 + 原生 Touch 事件手势,彻底绕开分页宽度问题。 +- **Modal 去掉 `transparent` prop**:`transparent={true}` 在 Android 上使 Modal 继承父容器的渲染上下文(右侧面板),只遮盖面板区域而非全屏。改为非透明 Modal + 黑色背景,强制创建独立原生全屏窗口。 diff --git a/.blueprint/modules/shared-types.md b/.blueprint/modules/shared-types.md index 59ff51c..c93a5cc 100644 --- a/.blueprint/modules/shared-types.md +++ b/.blueprint/modules/shared-types.md @@ -17,11 +17,11 @@ status: done - files: packages/types/package.json, packages/types/src/index.ts - acceptance: packages/types 仅包含 .d.ts 或 type-only TS 文件,被引入时不产生任何运行时代码 -### ✅ 通用业务类型 +### ✅ 核心业务类型 - status: done - complexity: S -- files: packages/types/src/common.ts, packages/types/src/survey.ts -- acceptance: 定义 ApiResponse、PaginatedResult、Nullable 等通用泛型类型,以及 Marker/Item/Photo 业务类型 +- files: packages/types/src/survey.ts +- acceptance: 定义 FloorNumber(1|2|3|4)、Marker(含 color)、Item(含 unit / spec / confirmedAt / referencePrice)、ItemWithPhotoCount、Photo 业务类型;所有字段与 SQLite schema 保持一致 ### 💭 环境与配置类型 - status: concept diff --git a/.blueprint/modules/survey-app-shell.md b/.blueprint/modules/survey-app-shell.md index 261177d..fcadffb 100644 --- a/.blueprint/modules/survey-app-shell.md +++ b/.blueprint/modules/survey-app-shell.md @@ -7,47 +7,47 @@ status: done ## 概述 -React Native (Expo SDK 52 + Expo Router) 应用骨架,实现 Z Fold 6 折叠/展开双形态布局切换。折叠时单栏显示,展开时左侧地图 + 右侧表单双栏并排。 +React Native (Expo SDK 52 + Expo Router) 应用骨架,实现 Z Fold 6 折叠/展开双形态布局切换。折叠时单栏显示,展开时左侧地图 + 右侧表单双栏并排。底部导航含「登记」「汇总」「设置」三个 Tab。 ## 任务卡 -### 🔵 Expo 应用骨架初始化 +### ✅ Expo 应用骨架初始化 - status: done - complexity: M -- files: apps/property-survey/app.json, apps/property-survey/package.json, apps/property-survey/app/_layout.tsx, apps/property-survey/app/index.tsx, apps/property-survey/app/summary.tsx +- files: apps/property-survey/app.json, apps/property-survey/package.json, apps/property-survey/app/_layout.tsx, apps/property-survey/app/index.tsx, apps/property-survey/app/summary.tsx, apps/property-survey/app/settings.tsx - depends: local-data-layer -- acceptance: `pnpm --filter property-survey start` 能在 Expo Go 或开发构建中启动,底部导航可在「登记」和「汇总」两个页面间切换,SQLite 数据库在启动时完成初始化 +- acceptance: `pnpm --filter property-survey start` 能启动;底部导航在「登记」「汇总」「设置」三个页面间切换;根布局包裹 CategoriesProvider;SQLite 数据库在启动时完成初始化 -### 🔵 折叠屏状态感知 Hook +### ✅ 折叠屏状态感知 Hook - status: done - complexity: S - files: apps/property-survey/src/layout/useFoldState.ts -- acceptance: useFoldState 返回 `{ isUnfolded: boolean, windowWidth: number }`,以 720px 为断点区分折叠/展开状态,useWindowDimensions 变化时自动更新,在模拟器宽度调整时可观察到状态切换 +- acceptance: useFoldState 返回 `{ isUnfolded: boolean, windowWidth: number }`,以 720px 为断点区分折叠/展开状态,useWindowDimensions 变化时自动更新 -### 🔵 双栏/单栏自适应布局容器 +### ✅ 双栏/单栏自适应布局容器 - status: done - complexity: M - files: apps/property-survey/src/layout/FoldableLayout.tsx - depends: survey-app-shell -- acceptance: 展开时渲染左右两栏(左侧 flexGrow:1 地图区,右侧 380px 详情区),折叠时只渲染左侧(地图全屏),接受 `left` 和 `right` 两个 ReactNode 插槽 +- acceptance: 展开时渲染左右两栏(左侧 flexGrow:1 地图区,右侧 260px 详情区动画展开);接受 `left` `right` `showDetail` 三个 prop;`showDetail` 控制右侧面板以 `LayoutAnimation.configureNext` 动画展开/收起(替代 JS 线程 Animated.timing,解决 Android 地图渲染空白及卡顿);折叠时只渲染左侧 -### 🔵 楼层切换标签组件 +### ✅ 楼层切换标签组件 - status: done - complexity: S - files: apps/property-survey/src/layout/FloorTabs.tsx - depends: survey-app-shell -- acceptance: 显示 1F / 2F / 3F 三个标签,选中项高亮,onChange 回调传出当前楼层数字(1/2/3),组件宽度自适应容器 +- acceptance: 显示 1F / 2F / 3F / 楼顶 四个标签;选中项高亮;onChange 回调传出当前楼层数字(1/2/3/4) ### ✅ 查看/编辑模式切换 - status: done - complexity: S - files: apps/property-survey/app/index.tsx - depends: survey-app-shell -- acceptance: 顶栏左侧显示「查看 | 编辑」分段控件;查看模式下右侧面板渲染 MarkerPhotoPanel,编辑模式下渲染 ItemFormSheet;折叠屏底部抽屉同步随模式切换;默认为编辑模式 +- acceptance: 顶栏左侧显示「查看 | 编辑」分段控件;查看模式下右侧面板渲染 MarkerPhotoPanel,编辑模式下渲染 ItemFormSheet;无选中标记时右侧面板为空(showDetail=false,动画收起);默认为编辑模式 -### ✅ 查看模式照片面板 +### ✅ 查看模式照片面板集成 - status: done -- complexity: M -- files: apps/property-survey/src/features/photo/MarkerPhotoPanel.tsx +- complexity: S +- files: apps/property-survey/app/index.tsx - depends: photo-capture -- acceptance: 展开屏内联右侧面板,折叠屏底部抽屉;按物品分组展示照片缩略图(3列网格),含物品名小标题;点击缩略图全屏预览,左右箭头切换;无照片物品显示「暂无照片」占位;无选中标记时面板为空白引导 +- acceptance: 选中标记点且处于查看模式时,showDetail=true,右侧面板渲染 MarkerPhotoPanel;切换楼层或模式时自动清空 selectedMarker diff --git a/CLAUDE.md b/CLAUDE.md index f099064..6bc0a59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,26 +1,38 @@ # react-phone-apps - + ## 项目蓝图 本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 -**每次对话都应遵循以下流程:** +### 对话开始时必须执行 -### 1. 了解当前状态 -对话开始时,先读取 `.blueprint/manifest.yaml` 了解项目模块和状态。 +1. 读取 `.blueprint/manifest.yaml` 了解项目全景和当前状态 +2. 读取 `.blueprint/CONVENTIONS.md` 了解完整工作流规则 -### 2. 讨论需求时更新蓝图 -- 确认了新需求 → 在 manifest.yaml 新增模块,创建 `.blueprint/modules/.md` -- 拆分了大需求 → 在模块文件中添加任务卡 -- 调整了方向 → 更新模块关系(edges) +### 需求讨论 → 蓝图同步规则 -### 3. 完成实现后标记蓝图 -- 完成了功能 → 更新对应任务卡状态为 `done`(前缀改为 ✅) -- 更新 manifest.yaml 顶部的 `updated` 日期 +| 用户说的 | Claude 应做的 | +|----------|--------------| +| "我想加个 XX 功能" | 确认需求后:manifest 新增模块(status: concept),创建 `.blueprint/modules/.md` | +| "XX 功能需要 A、B、C" | 在模块文件中拆分为任务卡 | +| "这个方案确定了" | 模块 status → planned,补充任务卡 files/acceptance | +| "开始做 XX" | 模块 status → in_progress | +| 没提蓝图但在讨论功能 | 主动提议更新蓝图 | -### 4. 任务卡规范 -详见 `.blueprint/CONVENTIONS.md`,核心规则: -- 任务卡同时具备 `files` + `acceptance` + complexity `S/M` → 标记为 📋 可派发 -- 可派发的任务卡可以直接交给 Claude Agent 独立完成 +### 复杂度决策(讨论新功能 / 拆解需求时) + +| complexity | 执行流程 | +|-----------|---------| +| S / M | 直接拆任务卡,确保每张卡有 `files` + `acceptance` + `complexity` → 标记 📋 | +| L(复杂系统设计) | 先 `/architect` 输出设计文档,用户确认后 `/splitter` 拆成 S/M 任务卡 | + +> L 级任务禁止直接写代码,必须先经过 `/architect` → `/splitter` 流程。 + +### 任务执行两阶段提交 + +- **开始写代码前**:任务卡前缀改为 🔵,status → in_progress +- **完成验收后**:任务卡前缀改为 ✅,status → done,更新 manifest `updated` 日期 + +完整规则(迭代管理、阻塞处理、复盘笔记等)详见 `.blueprint/CONVENTIONS.md`。 diff --git a/apps/property-survey/.expo/devices.json b/apps/property-survey/.expo/devices.json index 6fccd46..0cb37a4 100644 --- a/apps/property-survey/.expo/devices.json +++ b/apps/property-survey/.expo/devices.json @@ -2,7 +2,7 @@ "devices": [ { "installationId": "626cc0bf-3d4a-47d1-9fd9-50b31e88db04", - "lastUsed": 1775749877472 + "lastUsed": 1775754747519 } ] } diff --git a/apps/property-survey/android/app/src/main/AndroidManifest.xml b/apps/property-survey/android/app/src/main/AndroidManifest.xml index 2ff16d9..ee112c2 100644 --- a/apps/property-survey/android/app/src/main/AndroidManifest.xml +++ b/apps/property-survey/android/app/src/main/AndroidManifest.xml @@ -18,16 +18,7 @@ - - - - + diff --git a/apps/property-survey/package.json b/apps/property-survey/package.json index e9448b8..a6ca99b 100644 --- a/apps/property-survey/package.json +++ b/apps/property-survey/package.json @@ -17,6 +17,7 @@ "expo": "~52.0.0", "expo-build-properties": "^55.0.11", "expo-dev-client": "~5.0.20", + "expo-document-picker": "~13.0.2", "expo-file-system": "~18.0.0", "expo-image-picker": "~16.0.0", "expo-linking": "^7.0.5", diff --git a/apps/property-survey/src/features/map/FloorMap.tsx b/apps/property-survey/src/features/map/FloorMap.tsx index 193c149..6f50ffe 100644 --- a/apps/property-survey/src/features/map/FloorMap.tsx +++ b/apps/property-survey/src/features/map/FloorMap.tsx @@ -39,7 +39,6 @@ export interface MapSize { interface FloorMapProps { floor: FloorNumber markers: Marker[] - markerItemCounts?: Record onMarkerPress: (marker: Marker) => void selectedMarkerId?: number onMarkerDragEnd: (marker: Marker, x: number, y: number) => void @@ -49,7 +48,6 @@ interface FloorMapProps { export function FloorMap({ floor, markers, - markerItemCounts = {}, onMarkerPress, selectedMarkerId, onMarkerDragEnd, @@ -203,13 +201,13 @@ export function FloorMap({ /> ) })} - {markers.map((marker) => ( + {markers.map((marker, index) => ( onMarkerPress(marker)} onDragEnd={(x, y) => onMarkerDragEnd(marker, x, y)} diff --git a/apps/property-survey/src/features/map/MapMarker.tsx b/apps/property-survey/src/features/map/MapMarker.tsx index dc5b0df..13f1f5a 100644 --- a/apps/property-survey/src/features/map/MapMarker.tsx +++ b/apps/property-survey/src/features/map/MapMarker.tsx @@ -24,13 +24,13 @@ interface MapMarkerProps { marker: Marker mapSize: MapSize mapScale: SharedValue - itemCount?: number + seqNumber: number isSelected?: boolean onPress: () => void onDragEnd: (x: number, y: number) => void } -export function MapMarker({ marker, mapSize, mapScale, itemCount = 0, isSelected = false, onPress, onDragEnd }: MapMarkerProps) { +export function MapMarker({ marker, mapSize, mapScale, seqNumber, isSelected = false, onPress, onDragEnd }: MapMarkerProps) { const left = marker.x * mapSize.width - ICON_SIZE / 2 const top = marker.y * mapSize.height - ICON_SIZE @@ -134,11 +134,9 @@ export function MapMarker({ marker, mapSize, mapScale, itemCount = 0, isSelected - {itemCount > 0 && ( - - {itemCount} - - )} + + {seqNumber} + ) @@ -163,13 +161,14 @@ const styles = StyleSheet.create({ position: 'absolute', top: BADGE_OFFSET, right: BADGE_OFFSET, - minWidth: 12, - height: 12, - borderRadius: 6, - backgroundColor: '#EF4444', + minWidth: 13, + height: 13, + borderRadius: 7, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 2, + borderWidth: 1, + borderColor: '#fff', }, badgeText: { color: '#fff', diff --git a/apps/property-survey/src/features/map/MarkerList.tsx b/apps/property-survey/src/features/map/MarkerList.tsx index 9ba6c18..daef923 100644 --- a/apps/property-survey/src/features/map/MarkerList.tsx +++ b/apps/property-survey/src/features/map/MarkerList.tsx @@ -85,9 +85,10 @@ export function MarkerList({ floor, onMarkerPress, selectedMarkerId, refreshKey {markers.length === 0 && ( 长按地图{'\n'}添加标记 )} - {markers.map((m) => { + {markers.map((m, index) => { const isSelected = m.id === selectedMarkerId const dotColor = m.color ?? '#3B82F6' + const seqNumber = index + 1 return ( - {m.itemCount > 0 && ( - - {m.itemCount} - - )} + + {seqNumber} + ) })} @@ -137,7 +136,6 @@ export function MarkerList({ floor, onMarkerPress, selectedMarkerId, refreshKey [m.id, m.itemCount]))} onMarkerPress={onMarkerPress} selectedMarkerId={selectedMarkerId} onMarkerDragEnd={handleMarkerDragEnd} diff --git a/apps/property-survey/src/features/photo/PhotoPicker.tsx b/apps/property-survey/src/features/photo/PhotoPicker.tsx index a6abe73..083dbba 100644 --- a/apps/property-survey/src/features/photo/PhotoPicker.tsx +++ b/apps/property-survey/src/features/photo/PhotoPicker.tsx @@ -1,4 +1,4 @@ -import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native' +import { StyleSheet, TouchableOpacity, View } from 'react-native' import { Ionicons } from '@expo/vector-icons' import { usePhotoCapture } from './usePhotoCapture' @@ -10,26 +10,27 @@ interface PhotoPickerProps { export function PhotoPicker({ itemId, onAdded }: PhotoPickerProps) { const { capture } = usePhotoCapture(itemId, onAdded) - function showPicker() { - Alert.alert('添加照片', '请选择来源', [ - { text: '拍照', onPress: () => setTimeout(() => capture('camera'), 300) }, - { text: '从相册选择', onPress: () => setTimeout(() => capture('library'), 300) }, - { text: '取消', style: 'cancel' }, - ]) - } - return ( - - - + + capture('camera')} activeOpacity={0.7}> + + + capture('library')} activeOpacity={0.7}> + + + ) } const styles = StyleSheet.create({ - btn: { - width: '100%', - paddingVertical: 10, + row: { + flexDirection: 'row', + gap: 6, marginBottom: 8, + }, + btn: { + flex: 1, + paddingVertical: 10, borderWidth: 1.5, borderColor: '#D1D5DB', borderStyle: 'dashed', diff --git a/apps/property-survey/src/features/photo/usePhotoCapture.ts b/apps/property-survey/src/features/photo/usePhotoCapture.ts index 34d84e1..0499627 100644 --- a/apps/property-survey/src/features/photo/usePhotoCapture.ts +++ b/apps/property-survey/src/features/photo/usePhotoCapture.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' import * as ImagePicker from 'expo-image-picker' +import * as DocumentPicker from 'expo-document-picker' import * as FileSystem from 'expo-file-system' import { Alert } from 'react-native' import { insertPhoto } from '../../db/photos' @@ -26,48 +27,43 @@ async function copyToAppDir(sourceUri: string): Promise { export function usePhotoCapture(itemId: number, onAdded: () => void) { const capture = useCallback( async (source: 'camera' | 'library') => { - let permissionResult: ImagePicker.PermissionResponse - - if (source === 'camera') { - permissionResult = await ImagePicker.requestCameraPermissionsAsync() - } else { - permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync() - } - - if (!permissionResult.granted) { - Alert.alert( - '权限不足', - source === 'camera' ? '请在系统设置中开启相机权限' : '请在系统设置中开启相册权限', - ) - return - } - - const pickOptions: ImagePicker.ImagePickerOptions = { - mediaTypes: ImagePicker.MediaType.Images, - allowsEditing: false, - quality: JPEG_QUALITY, - exif: false, - base64: false, - allowsMultipleSelection: source === 'library', - } - - const result = - source === 'camera' - ? await ImagePicker.launchCameraAsync({ ...pickOptions, allowsMultipleSelection: false }) - : await ImagePicker.launchImageLibraryAsync(pickOptions) - - if (result.canceled) return - try { - for (const asset of result.assets) { - const manipulated = await resizeImage(asset.uri, asset.width ?? MAX_WIDTH) - const filePath = await copyToAppDir(manipulated) - await insertPhoto({ itemId, filePath, sortOrder: Date.now() }) + if (source === 'camera') { + const permissionResult = await ImagePicker.requestCameraPermissionsAsync() + if (!permissionResult.granted) { + Alert.alert('权限不足', '请在系统设置中开启相机权限') + return + } + const result = await ImagePicker.launchCameraAsync({ + mediaTypes: ['images'], + allowsEditing: false, + quality: JPEG_QUALITY, + exif: false, + base64: false, + allowsMultipleSelection: false, + }) + if (result.canceled) return + for (const asset of result.assets) { + const filePath = await copyToAppDir(await resizeImage(asset.uri, asset.width ?? MAX_WIDTH)) + await insertPhoto({ itemId, filePath, sortOrder: Date.now() }) + } + } else { + // 使用 document picker 绕过三星系统 Photo Picker 崩溃问题 + const result = await DocumentPicker.getDocumentAsync({ + type: 'image/*', + multiple: false, + copyToCacheDirectory: true, + }) + if (result.canceled) return + for (const asset of result.assets) { + const filePath = await copyToAppDir(asset.uri) + await insertPhoto({ itemId, filePath, sortOrder: Date.now() }) + } } onAdded() } catch (err) { - console.error('保存照片失败:', err) - Alert.alert('错误', '照片保存失败,请重试') + Alert.alert('错误', String(err)) + console.error('添加照片失败:', err) } }, [itemId, onAdded], diff --git a/apps/property-survey/src/layout/FoldableLayout.tsx b/apps/property-survey/src/layout/FoldableLayout.tsx index 5f33f90..cea260e 100644 --- a/apps/property-survey/src/layout/FoldableLayout.tsx +++ b/apps/property-survey/src/layout/FoldableLayout.tsx @@ -1,12 +1,7 @@ -import { type ReactNode, useRef } from 'react' -import { LayoutAnimation, Platform, StyleSheet, UIManager, View } from 'react-native' +import { type ReactNode } from 'react' +import { StyleSheet, View } from 'react-native' import { useFoldState } from './useFoldState' -// Android 需要手动开启 LayoutAnimation 支持 -if (Platform.OS === 'android') { - UIManager.setLayoutAnimationEnabledExperimental?.(true) -} - const DETAIL_PANEL_WIDTH = 260 interface FoldableLayoutProps { @@ -18,13 +13,6 @@ interface FoldableLayoutProps { export function FoldableLayout({ left, right, showDetail = false }: FoldableLayoutProps) { const { isUnfolded } = useFoldState() - // 在 props 变化引起的 re-render 之前触发原生布局动画 - const prevShowDetail = useRef(showDetail) - if (prevShowDetail.current !== showDetail) { - prevShowDetail.current = showDetail - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - } - if (!isUnfolded) { return {left} } @@ -41,7 +29,13 @@ export function FoldableLayout({ left, right, showDetail = false }: FoldableLayo const styles = StyleSheet.create({ single: { flex: 1 }, - dual: { flex: 1, flexDirection: 'row' }, - mapPane: { flexGrow: 1 }, - detailPane: { width: DETAIL_PANEL_WIDTH }, + dual: { flex: 1 }, + mapPane: { ...StyleSheet.absoluteFillObject }, + detailPane: { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + width: DETAIL_PANEL_WIDTH, + }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fa1f90..a25c582 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: expo-dev-client: specifier: ~5.0.20 version: 5.0.20(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)) + expo-document-picker: + specifier: ~13.0.2 + version: 13.0.3(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)) expo-file-system: specifier: ~18.0.0 version: 18.0.12(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)) @@ -2107,6 +2110,11 @@ packages: peerDependencies: expo: '*' + expo-document-picker@13.0.3: + resolution: {integrity: sha512-348xcsiA/YhgWm1SuJNNdb5cUDpRJYCyIk8MhOU2MEDxbVRR+Q1TiUBTCIMVqaWHcxsFQzP56Wwv9n24qjeILg==} + peerDependencies: + expo: '*' + expo-file-system@18.0.12: resolution: {integrity: sha512-HAkrd/mb8r+G3lJ9MzmGeuW2B+BxQR1joKfeCyY4deLl1zoZ48FrAWjgZjHK9aHUVhJ0ehzInu/NQtikKytaeg==} peerDependencies: @@ -6749,6 +6757,10 @@ snapshots: expo: 52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) expo-dev-menu-interface: 1.9.3(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)) + expo-document-picker@13.0.3(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)): + dependencies: + expo: 52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + expo-file-system@18.0.12(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)): dependencies: expo: 52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)