feat: 初始化 Property Survey 应用基础架构

- 添加 monorepo 结构 (apps/property-survey, packages/types)
- 配置 Expo + React Native + Android 构建链
- 实现本地数据层 (SQLite/better-sqlite3)
- 添加 Blueprint 项目蓝图系统
- 配置 ESLint + Prettier
- 添加共享类型定义
This commit is contained in:
lanrtop 2026-04-09 13:04:36 +09:00
parent a0929de768
commit 4e1d92264d
141 changed files with 17610 additions and 56 deletions

481
.blueprint/CONVENTIONS.md Normal file
View File

@ -0,0 +1,481 @@
---
version: 1.4.2
updated: 2026-04-08
source: dev-manager-tauri
---
# 蓝图更新规则
> ⚠️ 本文件由 dev-manager-tauri 统一管理,请勿手动修改。规则更新请在 dev-manager-tauri 项目中进行。
本文件定义了 `.blueprint/` 目录的维护规范,供 Claude 在对话中遵循。
## 目录结构
```
.blueprint/
├── manifest.yaml ← 模块列表、关系、布局坐标
├── CONVENTIONS.md ← 本文件Claude 行为规则)
└── modules/
└── <module-id>.md ← 每个模块的详情 + 任务卡
```
## 何时更新蓝图
| 事件 | 操作 |
|------|------|
| 用户确认了新需求 | 在 manifest.yaml 新增模块,创建 modules/<id>.md |
| **开始执行任务卡信息1** | **任务卡前缀改为 🔵status → in_progress第一行代码之前** |
| **完成了功能实现信息2** | **任务卡前缀改为 ✅status → done验收通过之后** |
| 拆分了大需求为子任务 | 在 modules/<id>.md 中添加任务卡 |
| 调整了架构方向 | 更新 edges 关系,可能调整 area |
| 放弃了某个方向 | 删除对应模块和文件,或标记 status: abandoned |
| **重构已完成的模块(迭代)** | **manifest.iteration 递增,旧任务卡保留,新建替代任务卡,模块加 `## 迭代记录`** |
## 模块状态定义
| 状态 | 含义 | 色标 |
|------|------|------|
| `done` | 已实现并验证 | 🟢 绿色 |
| `in_progress` | 正在开发 | 🔵 蓝色 |
| `planned` | 已规划,需求明确 | 🟡 黄色 |
| `concept` | 构思中,信息不完整 | ⚪ 灰色 |
| `blocked` | 执行受阻,需架构师介入 | 🔴 红色 |
## 任务卡格式
modules/<id>.md 中的每个二级标题(`###`)是一张任务卡:
```markdown
### 📋 任务标题
- status: todo
- complexity: M
- files: src/path/to/file.tsx, src-tauri/src/commands/xxx.rs
- depends: other-module-id
- acceptance: 简要描述完成标准
- blocked_reason: (仅 status: blocked 时填写)执行受阻的具体原因
- iteration: 1 # 可选,省略时继承 manifest 顶层 iteration 值
- replaces: 旧任务标题 # 可选,仅重构任务填写,说明本卡替代哪张旧卡
详细说明(可选)。
```
> **files 回填规则**:任务卡的 `files` 在规划时填入预期路径。实现完成后,若实际文件与规划有出入(合并、拆分、重命名),须在标记 ✅ done 之前将 `files` 更新为实际路径——这保证下次续接或派发时上下文准确。
### 状态前缀
| 前缀 | status | 含义 |
|------|--------|------|
| ✅ | done | 已完成信息1 + 信息2 均已写入) |
| 🔵 | in_progress | 执行中或被中断只有信息1等待信息2 |
| 📋 | todo | 可派发(信息充足) |
| 💭 | concept | 构思中 |
| 🔴 | blocked | 执行受阻 |
### 可派发标准(📋)
任务卡同时满足以下条件时标记为 📋:
1. status 为 `todo`
2. 有明确的 `files`(涉及哪些文件)
3. 有明确的 `acceptance`(验收标准)
4. complexity 为 `S``M`
不满足条件的 todo 任务保持无前缀,说明还需补充信息。
## manifest.yaml 规范
### 顶层字段
```yaml
version: 1
name: 项目名称
iteration: 1 # 当前迭代号(默认 1重构已完成模块时手动递增
updated: YYYY-MM-DD
```
### 模块字段
```yaml
- id: kebab-case-id # 唯一标识,同时是 modules/ 下的文件名
name: 显示名称
area: frontend # 所属领域 ID
status: done # done / in_progress / planned / concept
progress: 100 # 0~100done 自动视为 100
position: [x, y] # React Flow 画布坐标(可选)
```
### 边字段
```yaml
- from: module-a
to: module-b
type: dependency # dependency实线/ related虚线
```
### 领域字段
```yaml
- id: frontend
name: 前端
color: "#3B82F6" # 节点边框/分组背景色
```
## 进度计算
模块的 progress 由其任务卡的完成比例推算:
- 无任务卡的模块:手动设置 progress
- 有任务卡的模块(单迭代):`完成数 / 总数 × 100`(取整)
- 有任务卡的模块(多迭代):**只计当前 iteration 的任务卡**,旧迭代 done 任务不计入分母,避免历史完成率虚高
## 迭代管理
### 何时递增 iteration
**需要递增**(重构已完成的功能):
- 一个已有 ✅ done 任务卡的模块需要重新设计或重写
- 核心交互/架构方案被替换(不是在原有基础上扩展)
**不需要递增**(正常演进):
- 在现有模块上添加新功能
- 修复 bug
- 新增模块
### 重构时的操作步骤
1. **manifest.yaml 顶层** `iteration` +1更新 `updated` 日期
2. **旧任务卡**:保留原位,在卡片末尾加一行注释说明被替代(不改 status、不删除
```markdown
### ✅ 旧功能实现
- status: done
- iteration: 1
...
> ⚠️ 已被迭代 2「新功能实现」替代仅作历史记录。
```
3. **新任务卡**:新建替代卡,标注 `iteration``replaces`
```markdown
### 📋 新功能实现
- status: todo
- iteration: 2
- replaces: 旧功能实现
- complexity: M
- files: ...
- acceptance: ...
```
4. **模块文件**:追加 `## 迭代记录` 段落(见下方格式)
5. **模块 status**:重置为 `in_progress`progress 按当前迭代任务卡重新计算
### 迭代记录格式
模块文件末尾追加,每次迭代一条:
```markdown
## 迭代记录
### 迭代 2YYYY-MM-DD
- **原因**React Flow 在 50+ 节点时性能卡顿,重构为虚拟化渲染方案
- **替代**「React Flow 节点图渲染迭代1」→「节点图虚拟化渲染」
- **保留**迭代1的任务卡作为历史记录不删除
```
### 梦核如何感知迭代
- usage.json 快照携带 `iteration` 字段(见飞轮外环章节)
- 飞轮数据层按 iteration 分组计算完成率,迭代跳变(如 1→2 后完成率骤降)是正常信号
- 梦核 prompt 会展示「完成率跳变项目」摘要Opus 据此判断是主动重构还是项目失控
## 决策记录格式
模块文件中可添加 `## 决策记录` 段落,记录架构决策的 WHY
```markdown
## 决策记录
- 选择阿里云 OSS 而非 GitHub Releases 作为更新源,因为国内用户访问 GitHub 不稳定
- 使用 document-driven 蓝图而非数据库存储,因为 Claude 天然支持文件读写且可版本控制
```
桌面 App 会以琥珀色卡片展示在模块详情面板中。
## 飞轮工作流
Claude 在每次对话中应遵循以下流程:
### 阶段 1进入对话 — 读取蓝图
```
读取 .blueprint/manifest.yaml → 了解项目全景和当前状态
```
- 识别哪些模块 in_progress哪些有可派发任务
- 用简短话语告诉用户当前项目进展(如 "18/22 模块已完成2 个进行中"
### 阶段 2讨论需求 — 同步更新蓝图
| 用户说的 | Claude 应做的 |
|----------|--------------|
| "我想加个 XX 功能" | 确认需求后manifest 加模块 (status: concept),创建 modules/<id>.md |
| "XX 功能需要 A、B、C 三步" | 在模块文件中拆分为 3 张任务卡 |
| "这个方案确定了" | 模块 status → planned补充任务卡的 files/acceptance |
| "开始做 XX" | 模块 status → in_progress |
| "为什么选这个方案" | 记录到模块的 `## 决策记录` |
### 阶段 3实现功能 — 两阶段执行锁
任务卡执行遵循**两阶段提交**开始写第一行代码前加锁信息1全部完成后解锁信息2。只有信息1没有信息2说明任务被中断。
```
📋 todo
↓ 【信息1加锁】第一行代码改动之前先更新任务卡
🔵 in_progress
↓ 执行代码改动
├─ 成功 → 【信息2解锁】更新任务卡前缀为 ✅ → status: done
│ 所有任务卡完成 → 模块 status: done
│ 更新 manifest.yaml 的 updated 日期
├─ 失败且可自行解决 → 继续尝试,保持 🔵
└─ 失败且无法独立解决 → 🔴 blocked + blocked_reason → 留给架构师处理
```
#### 锁悬空时的续接规则
新会话读蓝图发现 🔵 任务卡只有信息1没有信息2
1. 检查任务卡的 `files` 字段,读取相关文件的**当前代码状态**
2. 判断已完成什么、还缺什么
3. 从断点继续执行,**不重头来过**
4. 完成后补写 ✅信息2关锁
#### 架构师处理 blocked 任务
当 Opus 读蓝图发现 🔴 blocked 任务时:
1. 分析 `blocked_reason`,定位根因
2. 根据情况采取行动:
- **拆**:任务本身拆得不好 → 标记原任务 abandoned → 产出新任务卡
- **补**:缺少前置依赖 → 创建前置任务卡 → 原任务加 depends
- **调**:架构方向需调整 → 更新模块设计/决策记录 → 重新拆任务卡
3. 新任务卡就绪后,原 blocked 任务可标记为 abandoned 或更新为 todo
### 阶段 4回顾 — 用户在桌面 App 查看
用户打开蓝图可视化:
- 看到全景进度
- 在「下一步」面板查看可派发任务
- 点击「复制派发」→ 粘贴给新 Claude 会话
### 飞轮的反馈信号
Claude 应关注这些信号来决定是否更新蓝图:
- 用户说"这个做完了" → 标记 done
- 用户说"这个不做了" → 标记 abandoned 或删除
- 用户说"这个要拆一下" → 拆分任务卡
- 用户说"记住这个决策" → 写入决策记录
- 用户没提蓝图但在讨论功能 → 主动提议更新蓝图
- 执行模型遇到无法解决的问题 → 标记 🔴 blocked + blocked_reason
- Opus 看到 blocked 任务 → 分析原因,拆/补/调后产出新任务卡
## 交互质量复盘笔记
> 本章节仅适用于**开发执行阶段**(实现代码时),初始化蓝图或同步蓝图时无需执行。
每完成一张 **M 或 L 复杂度**的任务卡后Claude 应通过 `append_project_note`MCP 工具写入一条结构化复盘笔记。这是飞轮外环的原始数据来源——积累足够多条后Claude 读一遍即可识别高频坑点和高效模式。
### 笔记格式
```
【复盘】<任务标题>
任务类型: 后端命令 | 前端组件 | 数据层 | MCP工具 | 架构调整
复杂度: S | M | L
实际轮数: N轮预估 M 轮)
是否返工: 是/否
返工原因: <若返工一句话说明根因"WSL路径格式未预料">
有效策略: <本次奏效的做法一句话>
遗留风险: <若有一句话>
```
### 触发时机
| 情况 | 操作 |
|------|------|
| M/L 任务卡标记为 ✅ done | 写一条复盘笔记source: mcp |
| 遇到非预期的系统边界或平台差异 | 额外写一条"陷阱记录",遗留风险填具体复现条件 |
| 同类问题第二次出现 | 在新笔记中引用旧笔记时间戳,说明"同类问题复发" |
### 示例
```
【复盘】WSL项目路径检测
任务类型: 后端命令
复杂度: M
实际轮数: 6轮预估 2 轮)
是否返工: 是
返工原因: wsl_path 字段存的是 Windows UNC 格式(\\wsl.localhost\Ubuntu-22.04\...),而非 Linux 路径,导致 wsl 子命令收到错误参数
有效策略: 在函数入口统一用 unc_to_distro_and_linux() 转换,一次解析,全程复用
遗留风险: Path::exists() 对 \\wsl.localhost\ 路径无效,所有 WSL 路径存在性检查必须走 wsl -d distro -- test -d/f
```
> 这些笔记不替代决策记录(决策记录记架构 WHY复盘笔记记执行 HOW 和踩坑)。两者都写。
---
## 飞轮外环(跨项目数据反哺)
内环描述的是单项目的执行流程。外环负责**把多个项目的执行数据聚合起来,反哺 CONVENTIONS 本身**。
### 数据采集usage.json 快照
每次 Claude 读取 `.blueprint/manifest.yaml`(通过炼境打开蓝图时触发),炼境自动向项目的 `.blueprint/usage.json` 追加一条当日快照:
```json
{
"project_name": "my-project",
"snapshots": [
{
"at": "2026-04-05T10:00:00Z",
"conventions_version": "1.3.0",
"iteration": 1,
"modules": { "total": 22, "done": 18, "in_progress": 2, "planned": 1, "concept": 1, "blocked": 0, "stalled": [] },
"tasks": { "total": 45, "done": 38, "in_progress": 3, "blocked": 0, "todo": 4, "dispatched": 3 },
"blocked_reasons": []
}
]
}
```
- `dispatched` = 当前满足📋可派发条件的任务数(有 files + acceptance + complexity S/M
- 节流:同一 UTC 日期只写一条,避免多次打开导致数据膨胀
- 文件存于项目本地,不上传服务器
### 数据聚合:飞轮指标
炼境跨项目读取所有 usage.json`conventions_version` 分组,对每个版本队列计算 4 项指标:
| 指标 | 计算方式 | 含义 |
|------|----------|------|
| 可派发率 | dispatched / total tasks | 任务卡规划质量,越高说明任务定义越清晰 |
| 任务完成率 | done / total tasks | 整体执行进度均值 |
| 飞轮持续率 | 30天内有快照的项目比例 | 外环是否在持续运转 |
| 阻塞密度 | blocked / total tasks | 系统性阻塞程度,越低越好 |
迭代感知:同一项目若 `iteration` 值增大,说明发生了主动重构。飞轮数据层对此类项目单独计算「当前迭代完成率」,与历史累计值区分展示,避免虚高。梦核 prompt 中标注「完成率跳变项目」供 Opus 分析是健康重构还是项目失控。
### 梦核分析:记忆巩固
炼境将聚合数据 + 当前 CONVENTIONS 全文格式化为提示词,发给 **Claude Opus** 做两件事:
- **固化**:哪些跨项目重复出现的阻塞模式、哪些有效实践,值得写进 CONVENTIONS
- **遗忘**哪些休眠项目90天无快照的 AI 文档可以归档,哪些 CONVENTIONS 规则已过时
### 闭环路径
```
执行任务 → usage.json 快照
炼境聚合指标
梦核提示词 → Claude Opus 分析
用户/维护者审核建议
更新 CONVENTIONS.md 源码 + 版本号
发布新版炼境
治理面板 sync_blueprint_rules → 分发到各项目
```
> **注意**CONVENTIONS.md 编译进炼境二进制,更新需要修改炼境源码并发版,不支持运行时修改。这是有意为之的设计——保证规则版本可追溯,防止随意污染。
## 复杂功能分析流程(/architect
当需求复杂度为 **L 级**时,在写任何代码之前执行本流程。
### 步骤
1. 读取 `manifest.yaml`,定位相关模块和依赖关系
2. 读取受影响的核心文件,理解现有实现模式和系统边界
3. 输出设计文档,格式如下:
```
## 设计决策:<功能名>
### 背景
<关联的现有模块>
### 方案选择
<2-3 个方案及取舍标注选定方案>
### 受影响文件
- 新增:<路径><用途>
- 修改:<路径><改动说明>
### 系统边界变更
<新增接口 / 数据表 / 字段定义>
### 风险点
- <风险><规避方式>
### 拆解建议
建议拆为 N 张任务卡依赖顺序卡A → 卡B → 卡C
```
4. **不写任何代码**,设计文档经用户确认后再进入拆解流程
---
## 任务拆解流程(/splitter
在设计文档确认后执行,产出可派发任务卡并写入蓝图文件。
### 步骤
1. 确认目标模块 id读取对应 `modules/<id>.md`
2. 按以下原则拆解:
- **粒度**:每张卡 complexity 必须是 S 或 M不允许 L
- **依赖顺序**:数据层 → 后端命令 → 前端封装 → UI 组件
- **可派发标准**:每张卡必须有 `files` + `acceptance` + `complexity`
3. 将任务卡追加到 `modules/<id>.md`
4. 更新 `manifest.yaml`status → `planned`,更新 `updated` 日期
5. 输出拆解摘要卡名、complexity、执行顺序
### 质量检查
- [ ] 每张卡有 `files`
- [ ] 每张卡有 `acceptance`
- [ ] complexity 全部是 S 或 M
- [ ] 有依赖的卡填了 `depends`
- [ ] 任务卡按执行顺序排列
---
## 技能依赖
本蓝图工作流依赖以下 Claude Code 技能处理 L 级复杂任务:
| 技能 | 触发词 | 用途 |
|------|--------|------|
| architect | `/architect` | 复杂功能架构分析,输出设计文档 |
| splitter | `/splitter` | 将设计文档拆解为可派发任务卡 |
### 检测方式
在 Claude Code 中输入 `/architect`,若 Claude 开始执行架构分析步骤则已就绪。
若提示找不到技能,按以下方式安装。
### 安装方式
技能文件随本项目分发,位于 `.claude/skills/`
```bash
# 复制到全局技能目录(任意项目均可使用)
cp -r .claude/skills/architect ~/.claude/skills/
cp -r .claude/skills/splitter ~/.claude/skills/
# 或仅在当前项目使用(.claude/skills/ 已存在则无需操作)
```
> 若通过炼境同步本蓝图规则,`.claude/skills/` 会随蓝图一并分发,无需手动安装。
---
## 注意事项
- 不要删除已完成的任务卡,它们是项目历史的一部分
- 模块 id 一旦确定尽量不要修改(会影响 edges 引用)
- 每次更新后修改 manifest.yaml 顶部的 `updated` 日期
- progress 由桌面 App 自动从任务卡完成比例计算,不需要手动维护
- 当用户的操作隐含了蓝图变更但没有明说时Claude 应主动提议更新(而非沉默)
- **批量执行完成后须做完整性检查**:同一会话内连续完成多张任务卡时,结束前必须逐一确认:① 每张已完成的子任务 status 改为 done前缀改为 ✅;② manifest.yaml 中对应模块的 status 已同步更新。两层缺一不可。(背景:单卡两阶段流程天然同步,批量执行时容易只改模块级 status 而漏掉子任务级更新,导致 App 看板 progress 数据失真)

View File

@ -1,7 +1,8 @@
version: 1
name: react-phone-apps
iteration: 1
updated: 2026-04-07
iteration: 2
updated: 2026-04-09
areas:
- id: infrastructure
@ -54,52 +55,52 @@ modules:
- id: shared-types
name: 共享类型定义
area: shared
status: concept
progress: 0
status: done
progress: 100
position: [380, 560]
# ── 数据层 ────────────────────────────────────────────────────
- id: local-data-layer
name: 本地数据持久化层
area: data
status: planned
progress: 0
status: done
progress: 100
position: [680, 80]
# ── RN 应用(不动产摸底)────────────────────────────────────
- id: survey-app-shell
name: 应用外壳与折叠屏布局
area: rn-app
status: planned
progress: 0
status: done
progress: 100
position: [980, 80]
- id: floor-map
name: 楼层地图与标记点管理
area: rn-app
status: planned
progress: 0
status: done
progress: 100
position: [980, 280]
- id: item-form
name: 物品登记表单
area: rn-app
status: planned
progress: 0
status: done
progress: 100
position: [980, 480]
- id: photo-capture
name: 照片采集与管理
area: rn-app
status: planned
progress: 0
status: done
progress: 100
position: [980, 680]
- id: inventory-summary
name: 汇总列表与统计
area: rn-app
status: planned
progress: 0
status: done
progress: 100
position: [980, 880]
# ── 前端应用Web预留──────────────────────────────────────

View File

@ -2,7 +2,7 @@
id: floor-map
name: 楼层地图与标记点管理
area: rn-app
status: planned
status: done
---
## 概述
@ -12,28 +12,28 @@ status: planned
## 任务卡
### 📋 平面图占位容器与坐标系
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/map/FloorMap.tsx
- depends: survey-app-shell
- acceptance: 渲染一个带灰色背景的矩形占位区域作为平面图,使用 onLayout 获取实际像素尺寸并转换相对坐标0~1容器宽高比固定为 4:3适配折叠/展开两种宽度
### 📋 长按手势添加标记点
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/map/useMarkerGesture.ts
- depends: floor-map
- acceptance: 在平面图区域长按 500ms 后触发,将触点位置转换为相对坐标并调用 insertMarker 写入数据库,新标记点立即在地图上显示,不误触短按事件
### 📋 标记点图标组件
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/map/MapMarker.tsx
- depends: floor-map
- acceptance: 在平面图上按相对坐标绝对定位渲染图钉图标,已有物品的标记点显示蓝色图钉 + 物品数量角标,空标记点显示灰色图钉,点击触发 onPress 回调
### 📋 当前楼层标记点数据加载
- status: todo
- status: done
- complexity: S
- files: apps/property-survey/src/features/map/MarkerList.tsx
- depends: floor-map

View File

@ -2,7 +2,7 @@
id: inventory-summary
name: 汇总列表与统计
area: rn-app
status: planned
status: done
---
## 概述
@ -12,29 +12,36 @@ status: planned
## 任务卡
### 📋 汇总数据查询 Hook
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/summary/useSummaryData.ts
- depends: local-data-layer
- acceptance: 查询所有 items JOIN markers JOIN photos返回带楼层、坐标、物品名、数量、单价、备注、照片数的扁平列表支持按 floor1/2/3/全部)和 name 关键字过滤;数据写入后调用 refresh() 触发重新查询
### 📋 汇总列表与筛选栏
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/summary/SummaryList.tsx, apps/property-survey/src/features/summary/FilterBar.tsx
- depends: inventory-summary
- acceptance: 列表每行显示楼层标签、物品名、数量、单价、备注1FilterBar 提供楼层选择器1F/2F/3F/全部和物品名搜索框滚动性能流畅FlatList空数据时显示引导文案
### 📋 统计汇总卡片
- status: todo
- status: done
- complexity: S
- files: apps/property-survey/src/features/summary/SummaryStats.tsx
- depends: inventory-summary
- acceptance: 固定在列表顶部,展示:登记点总数、物品种类数、总数量合计、总金额合计(数量×单价求和,单价为空则不计入),数值随筛选条件实时更新
### 📋 CSV 导出与系统分享
- status: todo
### CSV 导出与系统分享
- status: done
- complexity: M
- files: apps/property-survey/src/features/summary/exportData.ts
- depends: inventory-summary
- acceptance: 点击导出按钮,将当前筛选结果生成 UTF-8 BOM 的 CSV 文件(含表头:楼层/标记点名/物品名/数量/单价/备注1/备注2/照片数),写入临时目录后调用 expo-sharing 弹出系统分享面板,可发送到微信/邮件/云盘
### ✅ 导出照片关联索引
- status: done
- complexity: M
- files: apps/property-survey/src/features/summary/exportData.ts
- depends: inventory-summary
- acceptance: ZIP 结构改为:① 主 CSV 增加"照片目录"列,值为对应子目录路径(无照片则为空);② 照片按物品分子目录存放 `photos/{floor}F_{markerLabel}_{name}({itemId})/1.jpg`;③ ZIP 根目录生成 `photos/index.csv`每行对应一张照片物品ID/楼层/标记点/物品名/照片文件路径),方便程序化处理

View File

@ -2,7 +2,7 @@
id: item-form
name: 物品登记表单
area: rn-app
status: planned
status: done
---
## 概述
@ -12,21 +12,21 @@ status: planned
## 任务卡
### 📋 底部抽屉/右栏容器
- status: todo
- 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顶部显示标记点位置名称和所属楼层
### 📋 物品字段表单
- status: todo
- 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 调用
### 📋 标记点下物品列表
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/item/ItemList.tsx
- depends: item-form

View File

@ -2,7 +2,7 @@
id: local-data-layer
name: 本地数据持久化层
area: data
status: planned
status: done
---
## 概述
@ -12,27 +12,27 @@ status: planned
## 任务卡
### 📋 SQLite 初始化与 Schema 建表
- status: todo
- 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 三张表,重复启动不报错,字段与设计文档一致
### 📋 标记点 CRUD 操作
- status: todo
- 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
### 📋 物品记录 CRUD 操作
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/db/items.ts
- depends: local-data-layer
- acceptance: 提供 insertItem / getItemsByMarker / updateItem / deleteItem 四个函数,支持批量查询(传入 marker_id 数组),返回带关联照片数量的扩展类型
### 📋 照片路径 CRUD 操作
- status: todo
- status: done
- complexity: S
- files: apps/property-survey/src/db/photos.ts
- depends: local-data-layer

View File

@ -17,14 +17,14 @@ pnpm workspace 单体仓库基础配置,涵盖包管理、构建脚本和 Node
- files: package.json, pnpm-workspace.yaml
- acceptance: pnpm-workspace.yaml 正确声明 apps/* 和 packages/*,根 package.json 包含 dev/build/lint 脚本engines 约束 node>=18 pnpm>=8
### 💭 统一 TypeScript 基础配置
- status: concept
### 统一 TypeScript 基础配置
- status: done
- complexity: S
- files: tsconfig.base.json
- acceptance: 根目录提供 tsconfig.base.json各子包通过 extends 继承,启用 strict 模式
### 💭 统一 ESLint / Prettier 配置
- status: concept
### 统一 ESLint / Prettier 配置
- status: done
- complexity: S
- files: eslint.config.js, .prettierrc
- acceptance: 根目录提供共享 lint 规则,各子包可通过 overrides 扩展pnpm lint 从根目录一键运行

View File

@ -2,7 +2,7 @@
id: photo-capture
name: 照片采集与管理
area: rn-app
status: planned
status: done
---
## 概述
@ -12,14 +12,14 @@ status: planned
## 任务卡
### 📋 摄像头调用与图片压缩
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/photo/usePhotoCapture.ts
- depends: local-data-layer
- acceptance: 使用 expo-image-picker 提供「拍照」和「从相册选择」两种入口;拍摄/选择后自动压缩至最大宽度 1080px、JPEG quality 0.75;将压缩后的文件复制到 expo-file-system 的 App 专属目录;调用 insertPhoto 写入数据库;全流程有权限检查,拒绝权限时给出提示
### 📋 照片网格预览与删除
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/features/photo/PhotoGrid.tsx, apps/property-survey/src/features/photo/PhotoPicker.tsx
- depends: photo-capture

View File

@ -2,7 +2,7 @@
id: shared-types
name: 共享类型定义
area: shared
status: concept
status: done
---
## 概述
@ -11,17 +11,17 @@ status: concept
## 任务卡
### 💭 搭建类型包骨架
- status: concept
### 搭建类型包骨架
- status: done
- complexity: S
- files: packages/types/package.json, packages/types/src/index.ts
- acceptance: packages/types 仅包含 .d.ts 或 type-only TS 文件,被引入时不产生任何运行时代码
### 💭 通用业务类型
- status: concept
### 通用业务类型
- status: done
- complexity: S
- files: packages/types/src/common.ts
- acceptance: 定义 ApiResponse<T>、PaginatedResult<T>、Nullable<T> 等通用泛型类型,供所有 app 和 packages 使用
- files: packages/types/src/common.ts, packages/types/src/survey.ts
- acceptance: 定义 ApiResponse<T>、PaginatedResult<T>、Nullable<T> 等通用泛型类型,以及 Marker/Item/Photo 业务类型
### 💭 环境与配置类型
- status: concept

View File

@ -2,7 +2,7 @@
id: survey-app-shell
name: 应用外壳与折叠屏布局
area: rn-app
status: planned
status: done
---
## 概述
@ -12,28 +12,42 @@ React Native (Expo SDK 52 + Expo Router) 应用骨架,实现 Z Fold 6 折叠/
## 任务卡
### 📋 Expo 应用骨架初始化
- status: todo
- 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
- depends: local-data-layer
- acceptance: `pnpm --filter property-survey start` 能在 Expo Go 或开发构建中启动底部导航可在「登记」和「汇总」两个页面间切换SQLite 数据库在启动时完成初始化
### 📋 折叠屏状态感知 Hook
- status: todo
- status: done
- complexity: S
- files: apps/property-survey/src/layout/useFoldState.ts
- acceptance: useFoldState 返回 `{ isUnfolded: boolean, windowWidth: number }`,以 720px 为断点区分折叠/展开状态useWindowDimensions 变化时自动更新,在模拟器宽度调整时可观察到状态切换
### 📋 双栏/单栏自适应布局容器
- status: todo
- status: done
- complexity: M
- files: apps/property-survey/src/layout/FoldableLayout.tsx
- depends: survey-app-shell
- acceptance: 展开时渲染左右两栏(左侧 flexGrow:1 地图区,右侧 380px 详情区),折叠时只渲染左侧(地图全屏),接受 `left``right` 两个 ReactNode 插槽
### 📋 楼层切换标签组件
- status: todo
- status: done
- complexity: S
- files: apps/property-survey/src/layout/FloorTabs.tsx
- depends: survey-app-shell
- acceptance: 显示 1F / 2F / 3F 三个标签选中项高亮onChange 回调传出当前楼层数字1/2/3组件宽度自适应容器
### ✅ 查看/编辑模式切换
- status: done
- complexity: S
- files: apps/property-survey/app/index.tsx
- depends: survey-app-shell
- acceptance: 顶栏左侧显示「查看 | 编辑」分段控件;查看模式下右侧面板渲染 MarkerPhotoPanel编辑模式下渲染 ItemFormSheet折叠屏底部抽屉同步随模式切换默认为编辑模式
### ✅ 查看模式照片面板
- status: done
- complexity: M
- files: apps/property-survey/src/features/photo/MarkerPhotoPanel.tsx
- depends: photo-capture
- acceptance: 展开屏内联右侧面板折叠屏底部抽屉按物品分组展示照片缩略图3列网格含物品名小标题点击缩略图全屏预览左右箭头切换无照片物品显示「暂无照片」占位无选中标记时面板为空白引导

View File

@ -23,6 +23,154 @@
"todo": 0,
"total": 32
}
},
{
"at": "2026-04-08T04:06:08Z",
"blocked_reasons": [],
"conventions_version": "1.4.2",
"delta": {
"newly_blocked": [],
"newly_done": [
"floor-map/平面图占位容器与坐标系",
"floor-map/当前楼层标记点数据加载",
"floor-map/标记点图标组件",
"floor-map/长按手势添加标记点",
"inventory-summary/CSV 导出与系统分享",
"inventory-summary/汇总列表与筛选栏",
"inventory-summary/汇总数据查询 Hook",
"inventory-summary/统计汇总卡片",
"item-form/底部抽屉/右栏容器",
"item-form/标记点下物品列表",
"item-form/物品字段表单",
"local-data-layer/SQLite 初始化与 Schema 建表",
"local-data-layer/标记点 CRUD 操作",
"local-data-layer/照片路径 CRUD 操作",
"local-data-layer/物品记录 CRUD 操作",
"monorepo-setup/初始化 pnpm workspace",
"monorepo-setup/统一 ESLint / Prettier 配置",
"monorepo-setup/统一 TypeScript 基础配置",
"photo-capture/摄像头调用与图片压缩",
"photo-capture/照片网格预览与删除",
"shared-types/搭建类型包骨架",
"shared-types/通用业务类型",
"survey-app-shell/Expo 应用骨架初始化",
"survey-app-shell/双栏/单栏自适应布局容器",
"survey-app-shell/折叠屏状态感知 Hook",
"survey-app-shell/楼层切换标签组件"
],
"unblocked": []
},
"iteration": 2,
"modules": {
"blocked": 0,
"concept": 8,
"done": 8,
"in_progress": 0,
"planned": 0,
"stalled": [],
"total": 16
},
"task_ids": {
"blocked": [],
"done": [
"floor-map/平面图占位容器与坐标系",
"floor-map/当前楼层标记点数据加载",
"floor-map/标记点图标组件",
"floor-map/长按手势添加标记点",
"inventory-summary/CSV 导出与系统分享",
"inventory-summary/汇总列表与筛选栏",
"inventory-summary/汇总数据查询 Hook",
"inventory-summary/统计汇总卡片",
"item-form/底部抽屉/右栏容器",
"item-form/标记点下物品列表",
"item-form/物品字段表单",
"local-data-layer/SQLite 初始化与 Schema 建表",
"local-data-layer/标记点 CRUD 操作",
"local-data-layer/照片路径 CRUD 操作",
"local-data-layer/物品记录 CRUD 操作",
"monorepo-setup/初始化 pnpm workspace",
"monorepo-setup/统一 ESLint / Prettier 配置",
"monorepo-setup/统一 TypeScript 基础配置",
"photo-capture/摄像头调用与图片压缩",
"photo-capture/照片网格预览与删除",
"shared-types/搭建类型包骨架",
"shared-types/通用业务类型",
"survey-app-shell/Expo 应用骨架初始化",
"survey-app-shell/双栏/单栏自适应布局容器",
"survey-app-shell/折叠屏状态感知 Hook",
"survey-app-shell/楼层切换标签组件"
]
},
"tasks": {
"blocked": 0,
"dispatched": 0,
"done": 26,
"in_progress": 0,
"todo": 0,
"total": 53
}
},
{
"at": "2026-04-09T00:19:56Z",
"blocked_reasons": [],
"conventions_version": "1.4.2",
"delta": {
"newly_blocked": [],
"newly_done": [
"inventory-summary/导出照片关联索引"
],
"unblocked": []
},
"iteration": 2,
"modules": {
"blocked": 0,
"concept": 8,
"done": 8,
"in_progress": 0,
"planned": 0,
"stalled": [],
"total": 16
},
"task_ids": {
"blocked": [],
"done": [
"floor-map/平面图占位容器与坐标系",
"floor-map/当前楼层标记点数据加载",
"floor-map/标记点图标组件",
"floor-map/长按手势添加标记点",
"inventory-summary/CSV 导出与系统分享",
"inventory-summary/导出照片关联索引",
"inventory-summary/汇总列表与筛选栏",
"inventory-summary/汇总数据查询 Hook",
"inventory-summary/统计汇总卡片",
"item-form/底部抽屉/右栏容器",
"item-form/标记点下物品列表",
"item-form/物品字段表单",
"local-data-layer/SQLite 初始化与 Schema 建表",
"local-data-layer/标记点 CRUD 操作",
"local-data-layer/照片路径 CRUD 操作",
"local-data-layer/物品记录 CRUD 操作",
"monorepo-setup/初始化 pnpm workspace",
"monorepo-setup/统一 ESLint / Prettier 配置",
"monorepo-setup/统一 TypeScript 基础配置",
"photo-capture/摄像头调用与图片压缩",
"photo-capture/照片网格预览与删除",
"shared-types/搭建类型包骨架",
"shared-types/通用业务类型",
"survey-app-shell/Expo 应用骨架初始化",
"survey-app-shell/双栏/单栏自适应布局容器",
"survey-app-shell/折叠屏状态感知 Hook",
"survey-app-shell/楼层切换标签组件"
]
},
"tasks": {
"blocked": 0,
"dispatched": 0,
"done": 27,
"in_progress": 0,
"todo": 0,
"total": 54
}
}
]
}

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1610
.idea/caches/deviceStreaming.xml generated Normal file

File diff suppressed because it is too large Load Diff

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/react-phone-apps.iml" filepath="$PROJECT_DIR$/.idea/react-phone-apps.iml" />
</modules>
</component>
</project>

9
.idea/react-phone-apps.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

1
.npmrc
View File

@ -1,2 +1,3 @@
strict-peer-dependencies=false
auto-install-peers=true
node-linker=hoisted

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}

26
CLAUDE.md Normal file
View File

@ -0,0 +1,26 @@
# react-phone-apps
<!-- BLUEPRINT_MANAGED_START version="1.4.2" -->
## 项目蓝图
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
**每次对话都应遵循以下流程:**
### 1. 了解当前状态
对话开始时,先读取 `.blueprint/manifest.yaml` 了解项目模块和状态。
### 2. 讨论需求时更新蓝图
- 确认了新需求 → 在 manifest.yaml 新增模块,创建 `.blueprint/modules/<id>.md`
- 拆分了大需求 → 在模块文件中添加任务卡
- 调整了方向 → 更新模块关系edges
### 3. 完成实现后标记蓝图
- 完成了功能 → 更新对应任务卡状态为 `done`(前缀改为 ✅)
- 更新 manifest.yaml 顶部的 `updated` 日期
### 4. 任务卡规范
详见 `.blueprint/CONVENTIONS.md`,核心规则:
- 任务卡同时具备 `files` + `acceptance` + complexity `S/M` → 标记为 📋 可派发
- 可派发的任务卡可以直接交给 Claude Agent 独立完成
<!-- BLUEPRINT_MANAGED_END -->

View File

@ -0,0 +1,8 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

View File

@ -0,0 +1,14 @@
/* eslint-disable */
import * as Router from 'expo-router';
export * from 'expo-router';
declare module 'expo-router' {
export namespace ExpoRouter {
export interface __routes<T extends string | object = string> {
hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/summary`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; };
hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/`; params?: Router.UnknownOutputParams; } | { pathname: `/summary`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; };
href: Router.RelativePathString | Router.ExternalPathString | `/${`?${string}` | `#${string}` | ''}` | `/summary${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/summary`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; };
}
}
}

6
apps/property-survey/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli

16
apps/property-survey/android/.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
# Bundle artifacts
*.jsbundle

View File

@ -0,0 +1,176 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
// Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace 'com.example.propertysurvey'
defaultConfig {
applicationId 'com.example.propertysurvey'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
}
}
packagingOptions {
jniLibs {
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
}
}
androidResources {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
// Split option: 'foo,bar' -> ['foo', 'bar']
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
// Trim all elements in place.
for (i in 0..<options.size()) options[i] = options[i].trim();
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
options -= ""
if (options.length > 0) {
println "android.packagingOptions.$prop += $options ($options.length)"
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
options.each {
android.packagingOptions[prop] += it
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
}
}
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}

Binary file not shown.

View File

@ -0,0 +1,14 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@ -0,0 +1,34 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
<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_LAUNCH_WAIT_MS" android:value="0"/>
<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>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="property-survey"/>
<data android:scheme="com.example.propertysurvey"/>
<data android:scheme="exp+yljt-app"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,61 @@
package com.example.propertysurvey
import android.os.Build
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
super.onCreate(null)
}
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "main"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
){})
}
/**
* Align the back button behavior with Android S
* where moving root activities to background instead of finishing activities.
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
*/
override fun invokeDefaultOnBackPressed() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!moveTaskToBack(false)) {
// For non-root activities, use the default implementation to finish them.
super.invokeDefaultOnBackPressed()
}
return
}
// Use the default back button implementation on Android S
// because it's doing more than [Activity.moveTaskToBack] in fact.
super.invokeDefaultOnBackPressed()
}
}

View File

@ -0,0 +1,57 @@
package com.example.propertysurvey
import android.app.Application
import android.content.res.Configuration
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return packages
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
)
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,6 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
<item>
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
</item>
</layer-list>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1 @@
<resources/>

View File

@ -0,0 +1,6 @@
<resources>
<color name="splashscreen_background">#ffffff</color>
<color name="iconBackground">#ffffff</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#ffffff</color>
</resources>

View File

@ -0,0 +1,5 @@
<resources>
<string name="app_name">不动产摸底</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources>

View File

@ -0,0 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#ffffff</item>
</style>
<style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style>
</resources>

View File

@ -0,0 +1,41 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
ndkVersion = "26.1.10909125"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
}
}
apply plugin: "com.facebook.react.rootproject"
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
}
maven {
// Android JSC is installed from npm
url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist'))
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}

View File

@ -0,0 +1,59 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
org.gradle.java.home=C:\\Program Files\\Android\\Android Studio\\jbr
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
# Enable GIF support in React Native images (~200 B increase)
expo.gif.enabled=true
# Enable webp support in React Native images (~85 KB increase)
expo.webp.enabled=true
# Enable animated webp support (~3.4 MB increase)
# Disabled by default because iOS doesn't support animated webp
expo.webp.animated=false
# Enable network inspector
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false
expo.sqlite.enableFTS=true

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
apps/property-survey/android/gradlew vendored Normal file
View File

@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,38 @@
pluginManagement {
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString())
}
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
def command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'android'
].toList()
ex.autolinkLibrariesFromCommand(command)
}
}
rootProject.name = '不动产摸底'
dependencyResolutionManagement {
versionCatalogs {
reactAndroidLibs {
from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml")))
}
}
}
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
useExpoModules()
include ':app'
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile())

View File

@ -0,0 +1,53 @@
{
"expo": {
"name": "不动产摸底",
"slug": "yljt-app",
"owner": "lanrtops-organization",
"version": "1.0.0",
"orientation": "default",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"scheme": "property-survey",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.example.propertysurvey"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.example.propertysurvey"
},
"plugins": [
[
"expo-build-properties",
{
"android": {
"kotlinVersion": "1.9.25"
}
}
],
"expo-router",
[
"expo-sqlite",
{
"enableFTS": true
}
]
],
"experiments": {
"typedRoutes": true
},
"extra": {
"eas": {
"projectId": "27dc0343-a1df-4569-8019-82b2f061a420"
}
}
}
}

View File

@ -0,0 +1,55 @@
import { Tabs } from 'expo-router'
import { useEffect, useState } from 'react'
import { StatusBar } from 'expo-status-bar'
import { ActivityIndicator, View } from 'react-native'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { initDatabase } from '../src/db/database'
export default function RootLayout() {
const [dbReady, setDbReady] = useState(false)
useEffect(() => {
initDatabase()
.then(() => setDbReady(true))
.catch((err) => {
console.error('数据库初始化失败:', err)
setDbReady(true)
})
}, [])
if (!dbReady) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
)
}
return (
<SafeAreaProvider>
<StatusBar style="auto" />
<Tabs
screenOptions={{
headerShown: true,
tabBarActiveTintColor: '#10B981',
}}
>
<Tabs.Screen
name="index"
options={{
title: '登记',
tabBarLabel: '登记',
headerShown: false,
}}
/>
<Tabs.Screen
name="summary"
options={{
title: '汇总',
tabBarLabel: '汇总',
}}
/>
</Tabs>
</SafeAreaProvider>
)
}

View File

@ -0,0 +1,170 @@
import { useState } from 'react'
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { Ionicons } from '@expo/vector-icons'
import { FoldableLayout } from '../src/layout/FoldableLayout'
import { MarkerList } from '../src/features/map/MarkerList'
import { ItemFormSheet } from '../src/features/item/ItemFormSheet'
import { MarkerPhotoPanel } from '../src/features/photo/MarkerPhotoPanel'
import type { FloorNumber, Marker } from '@repo/types'
type AppMode = 'view' | 'edit'
const FLOORS: FloorNumber[] = [1, 2, 3]
export default function RegisterScreen() {
const insets = useSafeAreaInsets()
const [mode, setMode] = useState<AppMode>('edit')
const [currentFloor, setCurrentFloor] = useState<FloorNumber>(1)
const [selectedMarker, setSelectedMarker] = useState<Marker | null>(null)
const [markersRefreshKey, setMarkersRefreshKey] = useState(0)
function handleMarkerPress(marker: Marker) {
setSelectedMarker((prev) => (prev?.id === marker.id ? null : marker))
}
function handleModeChange(next: AppMode) {
setMode(next)
setSelectedMarker(null)
}
// 右栏:有标记选中时显示内容,否则显示引导占位
const rightPanel = selectedMarker
? mode === 'view'
? <MarkerPhotoPanel marker={selectedMarker} onClose={() => setSelectedMarker(null)} />
: <ItemFormSheet
marker={selectedMarker}
onClose={() => setSelectedMarker(null)}
onMarkerUpdated={() => setMarkersRefreshKey((k) => k + 1)}
/>
: <RightPlaceholder mode={mode} />
return (
<GestureHandlerRootView style={styles.root}>
{/* 顶栏 */}
<View style={[styles.topBar, { paddingTop: insets.top }]}>
<View style={styles.modeToggle}>
{(['view', 'edit'] as AppMode[]).map((m) => (
<TouchableOpacity
key={m}
style={[styles.modeBtn, mode === m && styles.modeBtnActive]}
onPress={() => handleModeChange(m)}
activeOpacity={0.7}
>
<Text style={[styles.modeBtnText, mode === m && styles.modeBtnTextActive]}>
{m === 'view' ? '查看' : '编辑'}
</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.floorRow}>
{FLOORS.map((f) => (
<TouchableOpacity
key={f}
style={[styles.floorBtn, currentFloor === f && styles.floorBtnActive]}
onPress={() => setCurrentFloor(f)}
activeOpacity={0.7}
>
<Text style={[styles.floorBtnText, currentFloor === f && styles.floorBtnTextActive]}>
{f}F
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* 三栏主体 */}
<FoldableLayout
left={
<MarkerList
floor={currentFloor}
onMarkerPress={handleMarkerPress}
selectedMarkerId={selectedMarker?.id}
refreshKey={markersRefreshKey}
/>
}
right={rightPanel}
/>
</GestureHandlerRootView>
)
}
// 未选中标记时右栏的引导占位
function RightPlaceholder({ mode }: { mode: AppMode }) {
return (
<View style={placeholder.container}>
<Ionicons
name={mode === 'view' ? 'eye-outline' : 'create-outline'}
size={36}
color="#D1D5DB"
/>
<Text style={placeholder.text}>
{mode === 'view' ? '选择左侧标记点\n查看照片' : '选择左侧标记点\n开始编辑'}
</Text>
</View>
)
}
const styles = StyleSheet.create({
root: { flex: 1 },
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 12,
paddingBottom: 7,
backgroundColor: '#fff',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#E5E7EB',
},
modeToggle: {
flexDirection: 'row',
backgroundColor: '#F3F4F6',
borderRadius: 8,
padding: 2,
},
modeBtn: {
paddingHorizontal: 14,
paddingVertical: 5,
borderRadius: 6,
},
modeBtnActive: {
backgroundColor: '#fff',
shadowColor: '#000',
shadowOpacity: 0.08,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
elevation: 1,
},
modeBtnText: { fontSize: 13, fontWeight: '500', color: '#9CA3AF' },
modeBtnTextActive: { color: '#111827', fontWeight: '700' },
floorRow: { flexDirection: 'row', gap: 4 },
floorBtn: {
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 16,
backgroundColor: '#F3F4F6',
},
floorBtnActive: { backgroundColor: '#10B981' },
floorBtnText: { fontSize: 13, fontWeight: '600', color: '#6B7280' },
floorBtnTextActive: { color: '#fff' },
})
const placeholder = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
backgroundColor: '#FAFAFA',
borderLeftWidth: StyleSheet.hairlineWidth,
borderLeftColor: '#E5E7EB',
},
text: {
fontSize: 13,
color: '#9CA3AF',
textAlign: 'center',
lineHeight: 20,
},
})

View File

@ -0,0 +1,57 @@
import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native'
import { Ionicons } from '@expo/vector-icons'
import { useSummaryData } from '../src/features/summary/useSummaryData'
import { FilterBar } from '../src/features/summary/FilterBar'
import { SummaryStats } from '../src/features/summary/SummaryStats'
import { SummaryList } from '../src/features/summary/SummaryList'
import { exportWithPhotos } from '../src/features/summary/exportData'
import type { FloorNumber } from '@repo/types'
import { Stack, useFocusEffect } from 'expo-router'
import { useCallback } from 'react'
export default function SummaryScreen() {
const { rows, filter, setFilter, refresh } = useSummaryData()
useFocusEffect(useCallback(() => {
refresh()
}, []))
async function handleExport() {
try {
await exportWithPhotos(rows)
} catch (err) {
Alert.alert('导出失败', String(err))
}
}
return (
<>
<Stack.Screen
options={{
headerRight: () => (
<TouchableOpacity onPress={handleExport} style={styles.exportBtn}>
<Ionicons name="share-outline" size={22} color="#10B981" />
</TouchableOpacity>
),
}}
/>
<View style={styles.container}>
<SummaryStats rows={rows} />
<FilterBar
floor={filter.floor}
keyword={filter.keyword}
onFloorChange={(floor) =>
setFilter((f) => ({ ...f, floor: floor as FloorNumber | 0 }))
}
onKeywordChange={(keyword) => setFilter((f) => ({ ...f, keyword }))}
/>
<SummaryList rows={rows} />
</View>
</>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
exportBtn: { paddingHorizontal: 8 },
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.8 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 749 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,7 @@
module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
}
}

View File

@ -0,0 +1,25 @@
{
"cli": {
"version": ">= 12.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"android": {
"buildType": "app-bundle"
}
}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.8 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 755 KiB

Some files were not shown because too many files have changed in this diff Show More