20260512212217

This commit is contained in:
oneao committed 2026-05-12 21:22:17 +08:00
1 parent c81f78880e
commit d2a0f90080
2 files changed
+389

No files matched your search

@@ -0,0 +1,304 @@
# 项目分析计划
## 一、项目概述
这是一个基于 **React Native (Expo)** 的跨平台移动应用,采用 **React Navigation** 路由 + **expo-router** 文件约定式路由架构。
## 二、技术栈
### 框架与核心库
| 库 | 版本 | 用途 |
| ------------ | --------- | ------- |
| expo | \~55.0.15 | 基础框架 |
| expo-router | \~55.0.12 | 文件约定式路由 |
| react-native | 0.83.4 | 原生视图层 |
| react | 19.2.0 | UI 框架 |
### 状态管理
| 库 | 版本 | 用途 |
| ----------------------------------------- | ------- | ------ |
| zustand | ^5.0.12 | 轻量状态管理 |
| @react-native-async-storage/async-storage | ^2.2.0 | 持久化存储 |
### 请求与数据
| 库 | 版本 | 用途 |
| -------------------- | ------- | --------- |
| alova | ^3.5.1 | 请求库 |
| @alova/adapter-axios | ^2.0.18 | Axios 适配器 |
| axios | ^1.15.0 | HTTP 客户端 |
### UI 组件
| 库 | 版本 | 用途 |
| ----------------------- | ------ | ------- |
| heroui-native | ^1.0.1 | UI 组件库 |
| lucide-react-native | ^1.8.0 | 图标库 |
| react-native-reanimated | 4.2.1 | 动画库 |
| tailwindcss | ^4.2.2 | 原子化 CSS |
## 三、项目架构
### 3.1 目录结构
```
src/
├── app/ # 页面 (expo-router 文件约定式路由)
│ ├── (tabs)/ # 底部 Tab 页面组
│ │ ├── home/ # 首页
│ │ ├── chat/ # 聊天页
│ │ ├── star/ # 收藏页
│ │ ├── my/ # 个人中心
│ │ └── _layout.tsx # Tab 布局
│ ├── auth/ # 认证页面
│ │ └── login.tsx # 登录页
│ ├── family/ # 家庭模块
│ │ ├── entry.tsx # 家庭入口(创建/加入)
│ │ ├── settings.tsx # 家庭设置
│ │ ├── user.tsx # 家庭成员
│ │ └── member/[id].tsx # 成员详情
│ ├── finance/ # 记账模块
│ │ ├── (tabs)/ # 记账子 Tab
│ │ │ ├── home/ # 记账首页
│ │ │ ├── analysis/ # 统计分析
│ │ │ └── _layout.tsx
│ │ ├── add/ # 新增记账
│ │ └── sub/ # 订阅管理
│ ├── tools/ # 工具页面
│ ├── _layout.tsx # 根布局
│ └── index.tsx # 根页面
├── components/ # 组件
│ ├── iconfont/ # IconFont 图标组件
│ └── layout/ # 布局组件 (Navbar, Tabbar)
├── configs/ # 配置
│ └── pages.ts # 子应用配置
├── hooks/ # 自定义 Hooks
│ ├── use-color-scheme.ts
│ └── use-theme-color.ts
├── layouts/ # 布局模板
│ ├── AppLayout.tsx # App 主布局 (带 Navbar + Tabbar)
│ ├── ModuleLayout.tsx # 模块布局 (独立功能模块)
│ └── PageLayout.tsx # 页面布局
├── request/ # 请求相关
│ ├── api.ts # API 接口定义
│ └── index.ts # Alova 实例
├── store/ # Zustand 状态库
│ ├── app.ts # App 全局状态
│ ├── user.ts # 用户状态
│ ├── finance.ts # 记账状态
│ └── family.ts # 家庭状态
├── types/ # TypeScript 类型
│ └── auth.ts # 认证类型
└── utils/ # 工具函数
├── http.ts # HTTP 封装
├── time.ts # 时间处理
└── cn.ts # 样式合并
```
### 3.2 路由架构
```
根布局 (_layout.tsx)
├── 认证流程
│ └── auth/login.tsx (未登录跳转)
├── 主应用 (已登录)
│ ├── (tabs) 底部 Tab 导航
│ │ ├── home (首页)
│ │ ├── space (空间)
│ │ ├── tools (工具中心 - 中心按钮)
│ │ ├── chat (聊天)
│ │ └── my (我的)
│ └── 独立页面
│ ├── family/entry (家庭入口)
│ ├── family/settings (家庭设置)
│ └── finance/(tabs) (记账模块 Tab)
│ ├── home (记账)
│ ├── add (新增 - 中心按钮)
│ └── analysis (分析)
```
### 3.3 布局系统
| 布局组件 | 用途 | 层级 |
| ------------ | ------------------------ | ---- |
| AppLayout | 主应用布局,提供 Navbar + Tabbar | 页面容器 |
| ModuleLayout | 功能模块布局,独立 Tabbar | 模块容器 |
| PageLayout | 页面基础布局,处理安全区域 | 内容容器 |
## 四、API 接口分析
### 4.1 请求封装
使用 **Alova** 请求库,配合 **Axios** 适配器。请求基地址: `EXPO_PUBLIC_API_URL` (开发环境: `https://dev.api.com`)
### 4.2 HTTP 拦截器
**请求拦截器**:
* 白名单接口直接放行 (`/auth/login/**`)
* 非白名单接口检查登录状态
* 未登录拦截并跳转登录页
* 已登录则注入 JWT Token 到 Header
**响应拦截器**:
* `code: 1000` → 成功
* `code: 1001` → Token 过期,跳转登录
* `code: 2000` → 业务错误,显示错误提示
* `code: 3000` → 系统错误,显示错误提示
* 其他 → 请求失败
### 4.3 核心 API
| 接口 | 方法 | 参数 | 用途 |
| --------------------- | ---- | --------------------------------------------------------------- | ------- |
| `/auth/login/qq` | POST | openid, nickname, avatar | QQ 登录 |
| `/auth/login/wechat` | POST | code | 微信登录 |
| `/bind/qq` | POST | openid, nickname, avatar | 绑定 QQ |
| `/bind/wechat` | POST | code | 绑定微信 |
| `/data/getUniqueId` | POST | count | 获取自增 ID |
| `/data/loadData` | POST | view\_name, search\_condition, order\_by, search\_columns, args | 查询数据 |
| `/data/loadDataBySql` | POST | sql, args | SQL 查询 |
| `/data/saveData` | POST | table\_name, key\_field, inserts, updates, deletes | 保存数据 |
| `/s3/upload` | POST | files | 文件上传 |
### 4.4 数据表结构 (推测)
| 表名 | 用途 |
| ----------------------------- | --------- |
| b\_family | 家庭表 |
| b\_family\_member | 家庭成员表 |
| b\_finance\_category | 记账分类表 |
| b\_finance\_category\_default | 默认记账分类 |
| b\_finance\_record | 记账记录 (推测) |
| v\_family\_member | 家庭成员视图 |
## 五、状态管理
### 5.1 Store 概览
| Store | 持久化 | 用途 |
| ------------ | -------------- | --------------- |
| userStore | ✅ AsyncStorage | 用户信息、Token、登录状态 |
| familyStore | ✅ AsyncStorage | 当前家庭 ID |
| financeStore | ❌ 内存 | 记账分类数据 |
| appStore | ❌ 内存 | App 全局状态 |
### 5.2 用户状态 (userStore)
```typescript
interface UserInfo {
id: number;
token: string;
nickname: string;
avatar: string;
}
```
### 5.3 家庭状态 (familyStore)
```typescript
interface Family {
family_id: number;
}
```
## 六、第三方登录
### 6.1 QQ 登录
* 使用 `expo-qq` 库
* 需要配置 App ID: `102826474`
### 6.2 微信登录
* 使用 `expo-wechat` 库
* 需要配置 App ID: `wxdab3e21a1f7e392f`
## 七、核心业务流程
### 7.1 登录流程
```
启动 → 检查登录状态
├── 未登录 → 跳转 /auth/login
│ ├── 微信登录 → 调接口 → 写入 Store → 跳转 /family/entry 或 /(tabs)/home
│ └── QQ 登录 → 调接口 → 写入 Store → 跳转 /family/entry 或 /(tabs)/home
│
└── 已登录 → 跳转 /(tabs)/home
```
### 7.2 家庭流程
```
首次登录 → /family/entry
├── 创建家庭 → 生成邀请码 → 保存家庭 + 成员 → 跳转 /(tabs)/home
└── 加入家庭 → 输入邀请码 → 保存成员 → 跳转 /(tabs)/home
```
### 7.3 记账流程
```
点击 Tab → /finance/home
├── 查看记录 → 列表展示
├── 新增记账 → 点击中心 "+" → /finance/add
│ ├── 选择类别
│ ├── 输入金额
│ ├── 选择日期
│ └── 保存
└── 数据分析 → 点击 "分析" Tab → /finance/analysis
```
## 八、环境配置
| 环境变量 | 开发环境 | 生产环境 |
| ------------------------ | --------------------------- | ----- |
| EXPO\_PUBLIC\_API\_URL | <https://dev.api.com> | (待配置) |
| EXPO\_PUBLIC\_G3\_URL | <http://118.89.70.199:9001> | (待配置) |
| EXPO\_PUBLIC\_G3\_BUCKET | test | (待配置) |
## 九、关键实现细节
### 9.1 路由跳转
* 使用 `expo-router` 的 `useRouter` 和 `router.push/replace`
* 动态路由: `/family/member/[id].tsx`
### 9.2 安全区域
* 使用 `useSafeAreaInsets()` 处理 iOS 刘海屏和 Android 挖孔屏
* Navbar 和 Tabbar 绝对定位,内容区占满屏幕
### 9.3 动画
* 使用 `react-native-reanimated` 实现 Tabbar 点击缩放动画
* `withTiming` 控制动画时长
### 9.4 主题
* 支持浅色/深色模式
* 使用 `useIsDark()` hook 判断当前主题
## 十、后续对话需关注的重点
1. **API 接口**: 所有后端交互通过 `src/request/api.ts` 封装
2. **状态管理**: 核心状态使用 Zustand + AsyncStorage 持久化
3. **路由**: 使用 expo-router 文件约定式路由
4. **UI 组件**: 主要使用 heroui-native 组件库
5. **登录机制**: JWT Token 存储在 userStore,通过 http.ts 拦截器自动注入
@@ -0,0 +1,85 @@
# 计划:在 addInsertRows 中添加跳转到新增行第一页的功能
## 需求分析
在 `addInsertRows` 函数执行后,自动跳转到新增行所在的第一个页面。如果当前页已经是新增行所在的页,则不进行跳转。
## 代码分析
### 相关函数位置
* **addInsertRows**: [feegrid-manager.js:832-843](file:///d:/workspace/code/jsp/js/feegrid-manager.js#L832-L843)
* **loadPage**: [feegrid-manager.js:644-743](file:///d:/workspace/code/jsp/js/feegrid-manager.js#L644-L743)
* **pagination 状态**: state.pagination(包含 pageNumber, pageSize, remoteTotal)
### 数据结构理解
1. `state.cacheManager.getInsertRows()` - 获取所有新增行(draftState === 'add')
2. `state.pagination.remoteTotal` - 远程数据的总条数
3. 新增行始终位于所有数据的最末端(即 remoteTotal 之后)
### 分页计算逻辑
* 总数据量 = remoteTotal + insertRows.length
* 新增行的起始位置 = remoteTotal
* 新增行第一行所在的页码 = Math.ceil((remoteTotal + 1) / pageSize)
* 或者:Math.floor(remoteTotal / pageSize) + 1
## 实现步骤
### Step 1: 修改 addInsertRows 函数
在 `addInsertRows(rows)` 函数末尾添加逻辑:
1. 获取所有新增行 `insertRows = state.cacheManager.getInsertRows()`
2. 如果 `insertRows.length === 0`,直接返回(不应该发生,但做防御性检查)
3. 计算新增行第一行所在的页码:
* `var remoteTotal = state.pagination.remoteTotal`
* `var pageSize = state.pagination.pageSize`
* `var firstInsertPage = Math.floor(remoteTotal / pageSize) + 1`
* (或者 `Math.ceil((remoteTotal + 1) / pageSize)`)
4. 获取当前页码 `var currentPage = state.pagination.pageNumber`
5. 如果 `firstInsertPage !== currentPage`,则调用 `loadPage(firstInsertPage)` 跳转
### Step 2: 代码放置位置
在 `addInsertRows` 函数的 for 循环之后(行842之后),在函数结束的大括号之前(行843之前)插入新逻辑。
## 预期代码变更
```javascript
function addInsertRows(rows) {
if (!$.isArray(rows)) rows = [rows];
var cm = state.cacheManager;
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var key = cm.getRowKey(row);
if (!cm.getRowFromCache(key)) {
cm.setRowToCache(row, true);
}
}
// 新增:跳转到新增行所在的第一页
var insertRows = state.cacheManager.getInsertRows();
if (insertRows.length > 0) {
var remoteTotal = state.pagination.remoteTotal;
var pageSize = state.pagination.pageSize;
var firstInsertPage = Math.floor(remoteTotal / pageSize) + 1;
var currentPage = state.pagination.pageNumber;
if (firstInsertPage !== currentPage) {
loadPage(firstInsertPage);
}
}
}
```