refactor: 标记点序号显示、布局叠加方式及照片选择器重构

- 标记点角标从物品数量改为按楼层顺序编号,角标背景色跟随标记颜色
- FoldableLayout 改为绝对定位叠加布局,移除 LayoutAnimation(解决 Android 地图渲染空白及卡顿)
- PhotoPicker 拆分为拍照/选图两个独立按钮;选图改用 expo-document-picker 绕过三星 Photo Picker 崩溃问题
- AndroidManifest 移除手动 FileProvider 声明(改由 Expo 插件自动注入)
- 同步更新蓝图文档:新增 category-management 模块、补全各模块已完成任务卡

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
lanrtop 2026-04-10 02:17:34 +09:00
parent cb7c5ad7f4
commit 3dbc6f0ff5
20 changed files with 276 additions and 174 deletions

View File

@ -1,6 +1,6 @@
--- ---
version: 1.4.2 version: 1.4.3
updated: 2026-04-08 updated: 2026-04-10
source: dev-manager-tauri source: dev-manager-tauri
--- ---

View File

@ -1,7 +1,7 @@
version: 1 version: 1
name: react-phone-apps name: react-phone-apps
iteration: 2 iteration: 2
updated: 2026-04-09 updated: 2026-04-10
areas: areas:
@ -103,6 +103,13 @@ modules:
progress: 100 progress: 100
position: [980, 880] position: [980, 880]
- id: category-management
name: 物品分类管理
area: rn-app
status: done
progress: 100
position: [980, 1080]
# ── 前端应用Web预留────────────────────────────────────── # ── 前端应用Web预留──────────────────────────────────────
- id: app-scaffold - id: app-scaffold
name: 应用脚手架模板 name: 应用脚手架模板
@ -202,6 +209,14 @@ edges:
to: photo-capture to: photo-capture
type: dependency type: dependency
# 分类管理
- from: local-data-layer
to: category-management
type: dependency
- from: category-management
to: item-form
type: dependency
# Web 前端(预留) # Web 前端(预留)
- from: monorepo-setup - from: monorepo-setup
to: app-scaffold to: app-scaffold

View File

@ -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 + 确认/取消);底部固定新增输入框 + 按钮;删除时弹确认框,说明已登记物品的分类标签不受影响

View File

@ -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原生 Imagefrom imageUri② SVG 矢量路径SvgXmloverdraw 预渲染提升缩放清晰度,动态计算安全 overdraw 倍数避免 Android Canvas OOM③ 用户嵌入图片(原生 Image从 SVG 提取并按 SVG matrix/rotate 变换还原坐标和旋转角)
### ✅ 捏合缩放 + 单指平移手势
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/features/map/FloorMap.tsx - files: apps/property-survey/src/features/map/FloorMap.tsx
- depends: survey-app-shell - acceptance: 使用 react-native-gesture-handler Gesture APIPinch 手势控制缩放MIN_SCALE=1, MAX_SCALE=5Pan 手势minPointers:1, maxPointers:1控制平移双指和单指通过 Gesture.Race 互斥;使用 react-native-reanimated SharedValue 实现原生线程动画
- acceptance: 渲染一个带灰色背景的矩形占位区域作为平面图,使用 onLayout 获取实际像素尺寸并转换相对坐标0~1容器宽高比固定为 4:3适配折叠/展开两种宽度
### 🔵 长按手势添加标记点 ### ✅ 长按添加标记点
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/features/map/useMarkerGesture.ts - files: apps/property-survey/src/features/map/FloorMap.tsx
- depends: floor-map - acceptance: LongPress 手势minDuration:500ms触点坐标反算缩放/平移变换得到地图相对坐标0~1调用 insertMarker 写入数据库;与双指手势通过 Gesture.Race 互斥
- acceptance: 在平面图区域长按 500ms 后触发,将触点位置转换为相对坐标并调用 insertMarker 写入数据库,新标记点立即在地图上显示,不误触短按事件
### 🔵 标记点图标组件 ### 标记点图标组件
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/features/map/MapMarker.tsx - files: apps/property-survey/src/features/map/MapMarker.tsx
- depends: floor-map - depends: floor-map
- acceptance: 在平面图上按相对坐标绝对定位渲染图钉图标,已有物品的标记点显示蓝色图钉 + 物品数量角标,空标记点显示灰色图钉,点击触发 onPress 回调 - acceptance: 按相对坐标绝对定位;已有物品的标记点显示彩色图钉 + 数量角标(颜色来自 marker.color选中状态有缩放高亮支持拖拽移动Pan 手势,拖拽结束调用 onDragEnd
### 🔵 当前楼层标记点数据加载 ### ✅ 楼层标记点数据加载与标记列表
- status: done - status: done
- complexity: S - complexity: S
- files: apps/property-survey/src/features/map/MarkerList.tsx - files: apps/property-survey/src/features/map/MarkerList.tsx
- depends: floor-map - 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 文件;提取背景 JPEGimageUri+ 用户嵌入图片embeddedImages含 cx/cy/width/height/angleDeg/opacity从 svgXml 中去除所有 image 元素及 Inkscape 私有命名空间;生成 floorMaps.generated.ts TypeScript 文件
## 决策记录
- **SVG 三层渲染架构**`SvgXml` 无法可靠渲染带 `rotate`/`matrix` transform 的 `<image>` 元素(坐标偏移、旋转不准)。改为构建时提取:① 背景 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 语义一致,无需额外偏移修正。

View File

@ -7,27 +7,34 @@ status: done
## 概述 ## 概述
点击地图标记点后弹出的物品编辑面板。折叠时以底部抽屉形式展现,展开时在右侧栏内联显示。支持单个标记点下的多条物品记录增删改。 点击地图标记点后弹出的物品编辑面板(右侧栏内联)。支持单个标记点下的多条物品记录增删改,物品字段含名称、动态分类、数量、单位、规格、单价、参考价、备注,以及确认状态标记
## 任务卡 ## 任务卡
### 🔵 底部抽屉/右栏容器 ### 底部抽屉/右栏容器
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/features/item/ItemFormSheet.tsx - files: apps/property-survey/src/features/item/ItemFormSheet.tsx
- depends: survey-app-shell, floor-map - depends: survey-app-shell, floor-map
- acceptance: 折叠时从屏幕底部滑出BottomSheet展开时在 FoldableLayout 右栏内渲染;接受 markerId 和 onClose 两个 prop顶部显示标记点位置名称和所属楼层 - acceptance: 右栏内渲染;顶部显示标记点标签 + 楼层 + 颜色选择器;头部操作区含复制标记点、删除标记点、关闭按钮
### 🔵 物品字段表单 ### 物品字段表单
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/features/item/ItemFields.tsx, apps/property-survey/src/features/item/useItemForm.ts - files: apps/property-survey/src/features/item/ItemFields.tsx, apps/property-survey/src/features/item/useItemForm.ts
- depends: item-form - depends: item-form, category-management
- acceptance: 包含物品名称必填文本、数量数字键盘、单价数字键盘可选、备注1多行文本、备注2多行文本五个字段保存时校验物品名称非空useItemForm 管理字段状态和 insertItem / updateItem 调用 - acceptance: 字段:物品名称(必填)、动态分类 chip来自 CategoriesContext、数量、单位、单价、参考价、规格多行、备注多行底部固定保存按钮 + 确认状态勾选框(显示确认时间或「待确认」);保存时校验物品名称非空;保存成功后显示 Toast 提示(「已保存」)
### 🔵 标记点下物品列表 ### 标记点下物品列表
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/features/item/ItemList.tsx - files: apps/property-survey/src/features/item/ItemList.tsx
- depends: item-form - 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 字段;标记点列表侧边栏同步显示每个物品的确认状态图标

View File

@ -7,33 +7,46 @@ status: done
## 概述 ## 概述
基于 Expo SQLite 的本地数据库层,管理标记点、物品记录和照片路径三张核心表。所有写操作使用事务保证原子性,供上层功能模块调用。 基于 Expo SQLite 的本地数据库层,管理标记点、物品记录、照片路径、物品分类四张核心表。所有写操作使用事务保证原子性,供上层功能模块调用。支持在线迁移:每次启动检测旧库结构并补列/重建表,无需用户操作。
## 任务卡 ## 任务卡
### 🔵 SQLite 初始化与 Schema 建表 ### SQLite 初始化与 Schema 建表
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/db/database.ts, apps/property-survey/src/db/schema.ts - 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 - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/db/markers.ts - files: apps/property-survey/src/db/markers.ts
- depends: local-data-layer - depends: local-data-layer
- acceptance: 提供 insertMarker / getMarkersByFloor / updateMarker / deleteMarker 四个函数deleteMarker 级联删除关联 items 和 photos所有函数返回类型安全的 Promise - acceptance: 提供 insertMarker / getMarkersByFloor / updateMarker / deleteMarker 四个函数deleteMarker 级联删除关联 items 和 photos所有函数返回类型安全的 Promisefloor 支持 1/2/3/4
### 🔵 物品记录 CRUD 操作 ### 物品记录 CRUD 操作
- status: done - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/db/items.ts - files: apps/property-survey/src/db/items.ts
- depends: local-data-layer - 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 - status: done
- complexity: S - complexity: S
- files: apps/property-survey/src/db/photos.ts - files: apps/property-survey/src/db/photos.ts
- depends: local-data-layer - depends: local-data-layer
- acceptance: 提供 insertPhoto / getPhotosByItem / deletePhoto / reorderPhotos 四个函数deletePhoto 同步删除设备文件系统中的对应图片文件 - 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 约束

View File

@ -7,20 +7,38 @@ status: done
## 概述 ## 概述
调用设备摄像头或相册为物品条目添加多张照片,照片压缩后存储到 App 专属目录,路径写入 SQLite photos 表。 调用设备摄像头或相册为物品条目添加多张照片,照片存储到 App 专属目录,路径写入 SQLite photos 表。支持 Android 13+ 权限模型CAMERA + READ_MEDIA_IMAGES + FileProvider
## 任务卡 ## 任务卡
### 🔵 摄像头调用与图片压缩 ### ✅ 摄像头调用与照片存储
- status: done - status: done
- complexity: M - 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 - depends: local-data-layer
- acceptance: 使用 expo-image-picker 提供「拍照」和「从相册选择」两种入口;拍摄/选择后自动压缩至最大宽度 1080px、JPEG quality 0.75;将压缩后的文件复制到 expo-file-system 的 App 专属目录;调用 insertPhoto 写入数据库;全流程有权限检查,拒绝权限时给出提示 - acceptance: 使用 expo-image-pickerMediaType.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+);配置 FileProviderandroid:authorities="${applicationId}.provider")支持相机临时文件 URIapp.json 中 expo-image-picker 插件配置中文权限说明
### ✅ 照片网格预览与删除(物品表单内)
- status: done - status: done
- complexity: M - 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 - 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 + 黑色背景,强制创建独立原生全屏窗口。

View File

@ -17,11 +17,11 @@ status: done
- files: packages/types/package.json, packages/types/src/index.ts - files: packages/types/package.json, packages/types/src/index.ts
- acceptance: packages/types 仅包含 .d.ts 或 type-only TS 文件,被引入时不产生任何运行时代码 - acceptance: packages/types 仅包含 .d.ts 或 type-only TS 文件,被引入时不产生任何运行时代码
### ✅ 通用业务类型 ### ✅ 核心业务类型
- status: done - status: done
- complexity: S - complexity: S
- files: packages/types/src/common.ts, packages/types/src/survey.ts - files: packages/types/src/survey.ts
- acceptance: 定义 ApiResponse<T>、PaginatedResult<T>、Nullable<T> 等通用泛型类型,以及 Marker/Item/Photo 业务类型 - acceptance: 定义 FloorNumber1|2|3|4、Marker含 color、Item含 unit / spec / confirmedAt / referencePrice、ItemWithPhotoCount、Photo 业务类型;所有字段与 SQLite schema 保持一致
### 💭 环境与配置类型 ### 💭 环境与配置类型
- status: concept - status: concept

View File

@ -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 - status: done
- complexity: M - 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 - depends: local-data-layer
- acceptance: `pnpm --filter property-survey start`在 Expo Go 或开发构建中启动,底部导航可在「登记」和「汇总」两个页面间切换,SQLite 数据库在启动时完成初始化 - acceptance: `pnpm --filter property-survey start`启动;底部导航在「登记」「汇总」「设置」三个页面间切换;根布局包裹 CategoriesProviderSQLite 数据库在启动时完成初始化
### 🔵 折叠屏状态感知 Hook ### 折叠屏状态感知 Hook
- status: done - status: done
- complexity: S - complexity: S
- files: apps/property-survey/src/layout/useFoldState.ts - 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 - status: done
- complexity: M - complexity: M
- files: apps/property-survey/src/layout/FoldableLayout.tsx - files: apps/property-survey/src/layout/FoldableLayout.tsx
- depends: survey-app-shell - 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 - status: done
- complexity: S - complexity: S
- files: apps/property-survey/src/layout/FloorTabs.tsx - files: apps/property-survey/src/layout/FloorTabs.tsx
- depends: survey-app-shell - depends: survey-app-shell
- acceptance: 显示 1F / 2F / 3F 三个标签选中项高亮onChange 回调传出当前楼层数字1/2/3组件宽度自适应容器 - acceptance: 显示 1F / 2F / 3F / 楼顶 四个标签选中项高亮onChange 回调传出当前楼层数字1/2/3/4
### ✅ 查看/编辑模式切换 ### ✅ 查看/编辑模式切换
- status: done - status: done
- complexity: S - complexity: S
- files: apps/property-survey/app/index.tsx - files: apps/property-survey/app/index.tsx
- depends: survey-app-shell - depends: survey-app-shell
- acceptance: 顶栏左侧显示「查看 | 编辑」分段控件;查看模式下右侧面板渲染 MarkerPhotoPanel编辑模式下渲染 ItemFormSheet折叠屏底部抽屉同步随模式切换;默认为编辑模式 - acceptance: 顶栏左侧显示「查看 | 编辑」分段控件;查看模式下右侧面板渲染 MarkerPhotoPanel编辑模式下渲染 ItemFormSheet无选中标记时右侧面板为空showDetail=false动画收起;默认为编辑模式
### ✅ 查看模式照片面板 ### ✅ 查看模式照片面板集成
- status: done - status: done
- complexity: M - complexity: S
- files: apps/property-survey/src/features/photo/MarkerPhotoPanel.tsx - files: apps/property-survey/app/index.tsx
- depends: photo-capture - depends: photo-capture
- acceptance: 展开屏内联右侧面板折叠屏底部抽屉按物品分组展示照片缩略图3列网格含物品名小标题点击缩略图全屏预览左右箭头切换无照片物品显示「暂无照片」占位无选中标记时面板为空白引导 - acceptance: 选中标记点且处于查看模式时showDetail=true右侧面板渲染 MarkerPhotoPanel切换楼层或模式时自动清空 selectedMarker

View File

@ -1,26 +1,38 @@
# react-phone-apps # react-phone-apps
<!-- BLUEPRINT_MANAGED_START version="1.4.2" --> <!-- BLUEPRINT_MANAGED_START version="1.4.3" -->
## 项目蓝图 ## 项目蓝图
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
**每次对话都应遵循以下流程:** ### 对话开始时必须执行
### 1. 了解当前状态 1. 读取 `.blueprint/manifest.yaml` 了解项目全景和当前状态
对话开始时,先读取 `.blueprint/manifest.yaml` 了解项目模块和状态。 2. 读取 `.blueprint/CONVENTIONS.md` 了解完整工作流规则
### 2. 讨论需求时更新蓝图 ### 需求讨论 → 蓝图同步规则
- 确认了新需求 → 在 manifest.yaml 新增模块,创建 `.blueprint/modules/<id>.md`
- 拆分了大需求 → 在模块文件中添加任务卡
- 调整了方向 → 更新模块关系edges
### 3. 完成实现后标记蓝图 | 用户说的 | Claude 应做的 |
- 完成了功能 → 更新对应任务卡状态为 `done`(前缀改为 ✅) |----------|--------------|
- 更新 manifest.yaml 顶部的 `updated` 日期 | "我想加个 XX 功能" | 确认需求后manifest 新增模块status: concept创建 `.blueprint/modules/<id>.md` |
| "XX 功能需要 A、B、C" | 在模块文件中拆分为任务卡 |
| "这个方案确定了" | 模块 status → planned补充任务卡 files/acceptance |
| "开始做 XX" | 模块 status → in_progress |
| 没提蓝图但在讨论功能 | 主动提议更新蓝图 |
### 4. 任务卡规范 ### 复杂度决策(讨论新功能 / 拆解需求时)
详见 `.blueprint/CONVENTIONS.md`,核心规则:
- 任务卡同时具备 `files` + `acceptance` + complexity `S/M` → 标记为 📋 可派发 | complexity | 执行流程 |
- 可派发的任务卡可以直接交给 Claude Agent 独立完成 |-----------|---------|
| S / M | 直接拆任务卡,确保每张卡有 `files` + `acceptance` + `complexity` → 标记 📋 |
| L复杂系统设计 | 先 `/architect` 输出设计文档,用户确认后 `/splitter` 拆成 S/M 任务卡 |
> L 级任务禁止直接写代码,必须先经过 `/architect``/splitter` 流程。
### 任务执行两阶段提交
- **开始写代码前**:任务卡前缀改为 🔵status → in_progress
- **完成验收后**:任务卡前缀改为 ✅status → done更新 manifest `updated` 日期
完整规则(迭代管理、阻塞处理、复盘笔记等)详见 `.blueprint/CONVENTIONS.md`
<!-- BLUEPRINT_MANAGED_END --> <!-- BLUEPRINT_MANAGED_END -->

View File

@ -2,7 +2,7 @@
"devices": [ "devices": [
{ {
"installationId": "626cc0bf-3d4a-47d1-9fd9-50b31e88db04", "installationId": "626cc0bf-3d4a-47d1-9fd9-50b31e88db04",
"lastUsed": 1775749877472 "lastUsed": 1775754747519
} }
] ]
} }

View File

@ -18,16 +18,7 @@
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/> <meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<provider <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@ -17,6 +17,7 @@
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-build-properties": "^55.0.11", "expo-build-properties": "^55.0.11",
"expo-dev-client": "~5.0.20", "expo-dev-client": "~5.0.20",
"expo-document-picker": "~13.0.2",
"expo-file-system": "~18.0.0", "expo-file-system": "~18.0.0",
"expo-image-picker": "~16.0.0", "expo-image-picker": "~16.0.0",
"expo-linking": "^7.0.5", "expo-linking": "^7.0.5",

View File

@ -39,7 +39,6 @@ export interface MapSize {
interface FloorMapProps { interface FloorMapProps {
floor: FloorNumber floor: FloorNumber
markers: Marker[] markers: Marker[]
markerItemCounts?: Record<number, number>
onMarkerPress: (marker: Marker) => void onMarkerPress: (marker: Marker) => void
selectedMarkerId?: number selectedMarkerId?: number
onMarkerDragEnd: (marker: Marker, x: number, y: number) => void onMarkerDragEnd: (marker: Marker, x: number, y: number) => void
@ -49,7 +48,6 @@ interface FloorMapProps {
export function FloorMap({ export function FloorMap({
floor, floor,
markers, markers,
markerItemCounts = {},
onMarkerPress, onMarkerPress,
selectedMarkerId, selectedMarkerId,
onMarkerDragEnd, onMarkerDragEnd,
@ -203,13 +201,13 @@ export function FloorMap({
/> />
) )
})} })}
{markers.map((marker) => ( {markers.map((marker, index) => (
<MapMarker <MapMarker
key={marker.id} key={marker.id}
marker={marker} marker={marker}
mapSize={mapSize} mapSize={mapSize}
mapScale={scale} mapScale={scale}
itemCount={markerItemCounts[marker.id] ?? 0} seqNumber={index + 1}
isSelected={marker.id === selectedMarkerId} isSelected={marker.id === selectedMarkerId}
onPress={() => onMarkerPress(marker)} onPress={() => onMarkerPress(marker)}
onDragEnd={(x, y) => onMarkerDragEnd(marker, x, y)} onDragEnd={(x, y) => onMarkerDragEnd(marker, x, y)}

View File

@ -24,13 +24,13 @@ interface MapMarkerProps {
marker: Marker marker: Marker
mapSize: MapSize mapSize: MapSize
mapScale: SharedValue<number> mapScale: SharedValue<number>
itemCount?: number seqNumber: number
isSelected?: boolean isSelected?: boolean
onPress: () => void onPress: () => void
onDragEnd: (x: number, y: number) => 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 left = marker.x * mapSize.width - ICON_SIZE / 2
const top = marker.y * mapSize.height - ICON_SIZE const top = marker.y * mapSize.height - ICON_SIZE
@ -134,11 +134,9 @@ export function MapMarker({ marker, mapSize, mapScale, itemCount = 0, isSelected
<Ionicons name="location" size={ICON_SIZE} color={markerColor} /> <Ionicons name="location" size={ICON_SIZE} color={markerColor} />
{itemCount > 0 && ( <View style={[styles.badge, { backgroundColor: markerColor }]}>
<View style={styles.badge}> <Text style={styles.badgeText}>{seqNumber}</Text>
<Text style={styles.badgeText}>{itemCount}</Text> </View>
</View>
)}
</Animated.View> </Animated.View>
</GestureDetector> </GestureDetector>
) )
@ -163,13 +161,14 @@ const styles = StyleSheet.create({
position: 'absolute', position: 'absolute',
top: BADGE_OFFSET, top: BADGE_OFFSET,
right: BADGE_OFFSET, right: BADGE_OFFSET,
minWidth: 12, minWidth: 13,
height: 12, height: 13,
borderRadius: 6, borderRadius: 7,
backgroundColor: '#EF4444',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingHorizontal: 2, paddingHorizontal: 2,
borderWidth: 1,
borderColor: '#fff',
}, },
badgeText: { badgeText: {
color: '#fff', color: '#fff',

View File

@ -85,9 +85,10 @@ export function MarkerList({ floor, onMarkerPress, selectedMarkerId, refreshKey
{markers.length === 0 && ( {markers.length === 0 && (
<Text style={styles.emptyHint}>{'\n'}</Text> <Text style={styles.emptyHint}>{'\n'}</Text>
)} )}
{markers.map((m) => { {markers.map((m, index) => {
const isSelected = m.id === selectedMarkerId const isSelected = m.id === selectedMarkerId
const dotColor = m.color ?? '#3B82F6' const dotColor = m.color ?? '#3B82F6'
const seqNumber = index + 1
return ( return (
<TouchableOpacity <TouchableOpacity
key={m.id} key={m.id}
@ -121,11 +122,9 @@ export function MarkerList({ floor, onMarkerPress, selectedMarkerId, refreshKey
))} ))}
</View> </View>
{m.itemCount > 0 && ( <View style={[styles.countBadge, { backgroundColor: dotColor }]}>
<View style={[styles.countBadge, { backgroundColor: dotColor }]}> <Text style={styles.countBadgeText}>{seqNumber}</Text>
<Text style={styles.countBadgeText}>{m.itemCount}</Text> </View>
</View>
)}
</TouchableOpacity> </TouchableOpacity>
) )
})} })}
@ -137,7 +136,6 @@ export function MarkerList({ floor, onMarkerPress, selectedMarkerId, refreshKey
<FloorMap <FloorMap
floor={floor} floor={floor}
markers={markers} markers={markers}
markerItemCounts={Object.fromEntries(markers.map((m) => [m.id, m.itemCount]))}
onMarkerPress={onMarkerPress} onMarkerPress={onMarkerPress}
selectedMarkerId={selectedMarkerId} selectedMarkerId={selectedMarkerId}
onMarkerDragEnd={handleMarkerDragEnd} onMarkerDragEnd={handleMarkerDragEnd}

View File

@ -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 { Ionicons } from '@expo/vector-icons'
import { usePhotoCapture } from './usePhotoCapture' import { usePhotoCapture } from './usePhotoCapture'
@ -10,26 +10,27 @@ interface PhotoPickerProps {
export function PhotoPicker({ itemId, onAdded }: PhotoPickerProps) { export function PhotoPicker({ itemId, onAdded }: PhotoPickerProps) {
const { capture } = usePhotoCapture(itemId, onAdded) 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 ( return (
<TouchableOpacity style={styles.btn} onPress={showPicker} activeOpacity={0.7}> <View style={styles.row}>
<Ionicons name="camera-outline" size={20} color="#9CA3AF" /> <TouchableOpacity style={styles.btn} onPress={() => capture('camera')} activeOpacity={0.7}>
</TouchableOpacity> <Ionicons name="camera-outline" size={20} color="#6B7280" />
</TouchableOpacity>
<TouchableOpacity style={styles.btn} onPress={() => capture('library')} activeOpacity={0.7}>
<Ionicons name="image-outline" size={20} color="#6B7280" />
</TouchableOpacity>
</View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
btn: { row: {
width: '100%', flexDirection: 'row',
paddingVertical: 10, gap: 6,
marginBottom: 8, marginBottom: 8,
},
btn: {
flex: 1,
paddingVertical: 10,
borderWidth: 1.5, borderWidth: 1.5,
borderColor: '#D1D5DB', borderColor: '#D1D5DB',
borderStyle: 'dashed', borderStyle: 'dashed',

View File

@ -1,5 +1,6 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import * as ImagePicker from 'expo-image-picker' import * as ImagePicker from 'expo-image-picker'
import * as DocumentPicker from 'expo-document-picker'
import * as FileSystem from 'expo-file-system' import * as FileSystem from 'expo-file-system'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { insertPhoto } from '../../db/photos' import { insertPhoto } from '../../db/photos'
@ -26,48 +27,43 @@ async function copyToAppDir(sourceUri: string): Promise<string> {
export function usePhotoCapture(itemId: number, onAdded: () => void) { export function usePhotoCapture(itemId: number, onAdded: () => void) {
const capture = useCallback( const capture = useCallback(
async (source: 'camera' | 'library') => { 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 { try {
for (const asset of result.assets) { if (source === 'camera') {
const manipulated = await resizeImage(asset.uri, asset.width ?? MAX_WIDTH) const permissionResult = await ImagePicker.requestCameraPermissionsAsync()
const filePath = await copyToAppDir(manipulated) if (!permissionResult.granted) {
await insertPhoto({ itemId, filePath, sortOrder: Date.now() }) 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() onAdded()
} catch (err) { } catch (err) {
console.error('保存照片失败:', err) Alert.alert('错误', String(err))
Alert.alert('错误', '照片保存失败,请重试') console.error('添加照片失败:', err)
} }
}, },
[itemId, onAdded], [itemId, onAdded],

View File

@ -1,12 +1,7 @@
import { type ReactNode, useRef } from 'react' import { type ReactNode } from 'react'
import { LayoutAnimation, Platform, StyleSheet, UIManager, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useFoldState } from './useFoldState' import { useFoldState } from './useFoldState'
// Android 需要手动开启 LayoutAnimation 支持
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental?.(true)
}
const DETAIL_PANEL_WIDTH = 260 const DETAIL_PANEL_WIDTH = 260
interface FoldableLayoutProps { interface FoldableLayoutProps {
@ -18,13 +13,6 @@ interface FoldableLayoutProps {
export function FoldableLayout({ left, right, showDetail = false }: FoldableLayoutProps) { export function FoldableLayout({ left, right, showDetail = false }: FoldableLayoutProps) {
const { isUnfolded } = useFoldState() const { isUnfolded } = useFoldState()
// 在 props 变化引起的 re-render 之前触发原生布局动画
const prevShowDetail = useRef(showDetail)
if (prevShowDetail.current !== showDetail) {
prevShowDetail.current = showDetail
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
}
if (!isUnfolded) { if (!isUnfolded) {
return <View style={styles.single}>{left}</View> return <View style={styles.single}>{left}</View>
} }
@ -41,7 +29,13 @@ export function FoldableLayout({ left, right, showDetail = false }: FoldableLayo
const styles = StyleSheet.create({ const styles = StyleSheet.create({
single: { flex: 1 }, single: { flex: 1 },
dual: { flex: 1, flexDirection: 'row' }, dual: { flex: 1 },
mapPane: { flexGrow: 1 }, mapPane: { ...StyleSheet.absoluteFillObject },
detailPane: { width: DETAIL_PANEL_WIDTH }, detailPane: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
width: DETAIL_PANEL_WIDTH,
},
}) })

12
pnpm-lock.yaml generated
View File

@ -31,6 +31,9 @@ importers:
expo-dev-client: expo-dev-client:
specifier: ~5.0.20 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)) 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: expo-file-system:
specifier: ~18.0.0 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)) 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: peerDependencies:
expo: '*' expo: '*'
expo-document-picker@13.0.3:
resolution: {integrity: sha512-348xcsiA/YhgWm1SuJNNdb5cUDpRJYCyIk8MhOU2MEDxbVRR+Q1TiUBTCIMVqaWHcxsFQzP56Wwv9n24qjeILg==}
peerDependencies:
expo: '*'
expo-file-system@18.0.12: expo-file-system@18.0.12:
resolution: {integrity: sha512-HAkrd/mb8r+G3lJ9MzmGeuW2B+BxQR1joKfeCyY4deLl1zoZ48FrAWjgZjHK9aHUVhJ0ehzInu/NQtikKytaeg==} resolution: {integrity: sha512-HAkrd/mb8r+G3lJ9MzmGeuW2B+BxQR1joKfeCyY4deLl1zoZ48FrAWjgZjHK9aHUVhJ0ehzInu/NQtikKytaeg==}
peerDependencies: 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: 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-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)): 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: 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: 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)