diff --git a/code/app/.workbuddy/memory/2026-06-27.md b/code/app/.workbuddy/memory/2026-06-27.md
new file mode 100644
index 00000000..c927c0cc
--- /dev/null
+++ b/code/app/.workbuddy/memory/2026-06-27.md
@@ -0,0 +1,55 @@
+# 2026-06-27
+
+## EmptyStatus loading 状态改用主题色
+- 修改文件:`app-rn/src/components/EmptyStatus.tsx`
+- LoadingVisual 中三处颜色改为主题色(accent = oklch(0.76 0.12 170),对应祖母绿 #10B981):
+ - 脉冲圆背景:`bg-foreground/10` → `bg-accent/10`
+ - 内圆背景:`bg-foreground/5` → `bg-accent/5`
+ - 加载图标颜色:`color="#111827"` → `color={accentColor}`(通过 `useThemeColor("accent")` 获取)
+- 新增 import:`useThemeColor` from `heroui-native`
+- 参考:项目中 tabbar.tsx、DateTimeRange.tsx、tools/index.tsx 均使用 `useThemeColor("accent")` 获取主题色实际值
+
+## 记账 home 列表样式优化
+- 修改文件:`app-rn/src/app/finance/(tabs)/home/index.tsx`
+- 列表不贴边:SectionView 的 contentContainerStyle 增加 `paddingHorizontal: 16`,白色列表项两侧留 16px 间距
+- 日期单行:section header 日期从「MM月DD日 + YYYY年」两行改为 `YYYY年MM月DD日` 一行
+- 收支单行:section header 收支合计从两行改为一行 `-¥支出 +¥收入`,去掉「支出/收入」文字标签
+- 列表项简化:移除金额下方的「收入/支出」子标签(+/- 符号已表达类型),同时删除不再使用的 `getCategoryTypeLabel` 函数
+
+## 记账统计页(analysis)优化
+- 修改文件:`app-rn/src/app/finance/(tabs)/analysis/index.tsx`
+- 收支趋势图增加数值显示:在图表下方新增 3 个白色卡片小条:
+ - 支出峰值:金额 + 峰值日期
+ - 收入峰值:金额 + 峰值日期
+ - 日均支出:金额 + 共 N 天
+ 数据来自新增的 `trendSummary` useMemo(maxExpense/maxIncome/对应 day、totalExpense/totalIncome、nonZeroDays)
+- 筛选条件扩展:FilterState 加上 `categoryId / memberId / type / keyword`,新增 `loadFilterOptions` 一次性加载 `b_finance_category`(status=1)和 `b_family_member`(join b_user 取 nickname)作为下拉选项
+- 使用 BottomSheet 筛选:照搬 home 模式,标题「筛选分析」+ 重置 + 日期范围 + 收支类型(全部/支出/收入)+ 成员 chips + 分类 chips + 备注关键词 + 取消/应用;筛选 chips 选中态用主题色 `bg-accent/10 border-accent` + `text-accent`
+- 筛选按钮改用主题色:原本 `bg-gray-900` → `bg-accent`(home 页和 analysis 页同步)
+- 移除「分析范围」字样:直接显示日期范围文本(与 home 保持一致)
+- SQL 增加过滤:categoryId → `f.category_id = ?`、memberId → `f.create_by = ?`、type → `c.type = ?`、keyword → `coalesce(f.remark,'') like ?`;trend/category/member 统计也跟随筛选条件变化
+- 移除未使用的 `useThemeColor` import(统一走 className `bg-accent`)
+
+
+## 统计页趋势图坐标轴修复 + 成员统计移除
+- 修改文件:`app-rn/src/app/finance/(tabs)/analysis/index.tsx`
+- 趋势图坐标轴标签显示修复:
+ - 下载 `Inter-Regular.ttf`(299KB)到 `app-rn/src/assets/fonts/`
+ - 通过 `useFont` 加载字体并传入 `CartesianChart` 的 `axisOptions.font`
+ - 调整 `chartPadding` 给轴标签留空间
+- 移除成员统计区块,保留 filter 中的成员筛选
+- 分类展示用列表 + 进度条(CategoryRow 组件)
+
+## 统计页趋势图改回折线 + 自定义 SVG 饼图
+- 删除字体文件 app-rn/src/assets/fonts/Inter-Regular.ttf 和 useFont 引用(用户要求不加载字体)
+- 趋势图改为双折线图:支出(#FB7185)+ 收入(#10B981),X 轴日期(MM/DD),Y 轴金额
+- 用 react-native-svg 自定义 CategoryPieChart 组件
+- 分类占比图例百分比按饼图自身总额计算
+
+## Home 刷新按钮 + 分析页筛选标签
+- **home**: 日期行新增刷新按钮(RotateCcwIcon),点击调用 handleRefresh
+- **home**: 分类/备注 chip 行移到日期行下方、合计卡片上方
+- **home**: chip 文本追加 numberOfLines={1} 防溢出
+- **analysis**: 日期行新增刷新按钮(替代原 reset 按钮,reset 仍在 BottomSheet 中可用)
+- **analysis**: 日期行下方新增 `activeFilterLabels` chip 行(分类/成员/类型/备注),溢出省略号
+- **analysis**: 折线图修复:Line 移除 chartBounds 传参、开启 connectMissingData={true}
diff --git a/code/app/.workbuddy/memory/2026-06-28.md b/code/app/.workbuddy/memory/2026-06-28.md
new file mode 100644
index 00000000..7a01f13c
--- /dev/null
+++ b/code/app/.workbuddy/memory/2026-06-28.md
@@ -0,0 +1,20 @@
+## 2026-06-28
+
+- **SQL 模板字面量 Metro 解析错误修复**:Expo/Babel 在 `.tsx` 文件中会将模板字面量中的 `id` 关键字误解析为 JSX 属性,报 `Missing semicolon`。根因是 `@babel/plugin-transform-typescript` 的 TSX 模式 bug。最终方案:将所有 SQL 模板字面量改为字符串拼接(`+` 连接),避免了模板字面量 → TSX 解析路径。涉及文件:
+ - `app-rn/src/app/finance/(tabs)/analysis/index.tsx` — 3 处 SQL
+ - `app-rn/src/app/finance/(tabs)/home/index.tsx` — 1 处 SQL
+ - `app-rn/src/app/finance/detail/[id].tsx` — 1 处 SQL
+- 注意:仅提取为变量不够(`const sql = \`...\`` 仍然触发),必须完全避免模板字面量
+- **中文乱码修复**:修复了以下文件中因编码转换导致的大量中文乱码(UTF-8 被误当 GBK/CP1252 解析导致的 mojibake):
+ - `app-rn/src/app/finance/(tabs)/analysis/index.tsx` — 约 50 处乱码修复(注释、UI 标签、¥ 符号)
+ - `app-rn/src/app/finance/(tabs)/home/index.tsx` — 约 20 处乱码修复(toast 消息、UI 标签、¥ 符号)
+ - `app-rn/src/app/finance/category/index.tsx` — 约 12 处乱码修复(toast 消息、表单标签)
+ - `app-rn/src/app/finance/add/index.tsx` — 约 15 处乱码修复(UI 标签、toast 消息、¥ 符号)
+- **修复 DateTime/DateTimeRange 乱码和日历外边距**:修正了中文注释和按钮标签乱码,日历容器添加 `px-4`
+- **移除 BottomSheet 默认内边距**:gorhom 的 `BottomSheetView` 自带 `padding: 16`,在 `AppBottomSheetContent` 中通过 `style={[{ padding: 0 }]}` 消除。补偿了 `family/settings.tsx` 中一个缺少 `px-4` 的 BottomSheet 使用处
+- **BottomSheet 内 input 键盘避让修复**:`family/user.tsx` 中 sheet 内的 heroui ``(底层原生 TextInput)被键盘遮挡。根因有三层:
+ 1. `BottomSheet.tsx` 封装时 `keyboardBehavior="extend"` 是错的——gorhom 源码(BottomSheet.js L585)显示 `extend` 只是把 sheet 拉到最高 snap point,**根本不上移**。应改为 gorhom 默认值 `"interactive"`(L602-608:`highestDetentPosition - keyboardHeightInContainer`,sheet 上移到键盘上方)。
+ 2. `android_keyboardInputMode="adjustResize"` 在 Android 上会导致 `interactive` 模式被**跳过**(L605: `!(Platform.OS === 'android' && android_keyboardInputMode === 'adjustResize')`)。应改为 gorhom 默认值 `"adjustPan"`。
+ 3. heroui `` 底层是原生 `TextInput`,未注册到 sheet 内部 context,sheet 感知不到 focus。
+ - 最终方案(最优):`BottomSheet.tsx` 改 `keyboardBehavior="interactive"` + `android_keyboardInputMode="adjustPan"`。`AppBottomSheetInput` 封装改用 heroui 原生 `Input` + `useBottomSheetAwareHandlers`(heroui-native 提供),hook 内部调用 gorhom `useBottomSheetInternal(true)` 把 focus/blur 注册到 `animatedKeyboardState`。这样保留 heroui Input 全部样式和 API,无需用 `BottomSheetTextInput`。
+ - 全项目 sheet 内 `` 统一替换为 ``:`family/user.tsx`(2处)、`family/settings.tsx`(2处)、`finance/add/index.tsx`(2处)、`finance/category/index.tsx`(1处)、`finance/(tabs)/home/index.tsx`(1处)、`finance/(tabs)/analysis/index.tsx`(1处)。
diff --git a/code/app/.workbuddy/memory/MEMORY.md b/code/app/.workbuddy/memory/MEMORY.md
index ee9b50c6..f29511db 100644
--- a/code/app/.workbuddy/memory/MEMORY.md
+++ b/code/app/.workbuddy/memory/MEMORY.md
@@ -16,3 +16,12 @@
- 不要卡片套卡片(no card nesting)
- 子应用 icon 不设彩色背景
- Section 标题统一用 `SectionLabel` 组件(13px semibold text-gray-800)
+
+## 已知库问题
+- **BottomSheet 初始化闪烁**:gorhom bottom-sheet 默认 `animateOnMount=true`,即使 `index=-1`(关闭状态)mount 时也会触发动画,首帧显示底部面板。修复:``。**不要用 `{isOpen && }` 条件渲染**,会导致 `BottomSheetContentContainer` 的 `useRef(isOpen)` 捕获错误初始值,首次打开时 `snapToIndex` 永远不会触发。
+- **模板字面量 `id` 解析错误**:Expo/Babel 在 `.tsx` 文件中会把模板字面量内的 `id,` 误解析为 JSX 属性,报 `Missing semicolon`。**规则:`.tsx` 文件中禁止在模板字面量(反引号)内使用 `id` 关键字**,必须改用字符串拼接(`'...' + '...'`)。仅提取为变量不够,必须完全避免模板字面量。
+- **BottomSheet 内 input 键盘避让**:三层根因,缺一不可:
+ 1. `keyboardBehavior` 必须是 `"interactive"`(gorhom 默认值),不能用 `"extend"`。gorhom 源码:`extend` 只拉到最高 snap point 不上移;`interactive` 才会 `highestDetent - keyboardHeight` 上移避让。
+ 2. `android_keyboardInputMode` 必须是 `"adjustPan"`(gorhom 默认值),不能用 `"adjustResize"`。源码 L605:Android + adjustResize 会**跳过** interactive 模式。
+ 3. sheet 内输入框必须接入 sheet 的键盘状态。最优方案:用 heroui 原生 `Input` + `useBottomSheetAwareHandlers`(从 `heroui-native` 导出),hook 内部调用 gorhom `useBottomSheetInternal(true)` 把 focus/blur 注册到 `animatedKeyboardState`。项目已封装为 `BottomSheet.Input`。
+ **规则:BottomSheet 内部的所有输入框统一用 `BottomSheet.Input`,`BottomSheet.tsx` 中 `keyboardBehavior="interactive"` + `android_keyboardInputMode="adjustPan"` 保持 gorhom 默认值。**
diff --git a/code/app/app-rn/.env.development b/code/app/app-rn/.env.development
index 946129c3..b8c3c1e9 100644
--- a/code/app/app-rn/.env.development
+++ b/code/app/app-rn/.env.development
@@ -1 +1 @@
-EXPO_PUBLIC_API_URL=http://192.168.5.12:8087
\ No newline at end of file
+EXPO_PUBLIC_API_URL=http://192.168.5.23:8087
\ No newline at end of file
diff --git a/code/app/app-rn/.env.production b/code/app/app-rn/.env.production
index 1a4c46c1..b8c3c1e9 100644
--- a/code/app/app-rn/.env.production
+++ b/code/app/app-rn/.env.production
@@ -1 +1 @@
-EXPO_PUBLIC_API_URL=https://prod.api.com
\ No newline at end of file
+EXPO_PUBLIC_API_URL=http://192.168.5.23:8087
\ No newline at end of file
diff --git a/code/app/app-rn/.oxlintrc.json b/code/app/app-rn/.oxlintrc.json
index 75868c2d..31e054fe 100644
--- a/code/app/app-rn/.oxlintrc.json
+++ b/code/app/app-rn/.oxlintrc.json
@@ -1,10 +1,6 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
- "plugins": [
- "typescript",
- "unicorn",
- "oxc"
- ],
+ "plugins": ["typescript", "unicorn", "oxc"],
"categories": {
"correctness": "error"
},
@@ -12,4 +8,4 @@
"env": {
"builtin": true
}
-}
\ No newline at end of file
+}
diff --git a/code/app/app-rn/app.json b/code/app/app-rn/app.json
index 2e576cc3..23060e12 100644
--- a/code/app/app-rn/app.json
+++ b/code/app/app-rn/app.json
@@ -6,11 +6,7 @@
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"userInterfaceStyle": "light",
- "scheme": [
- "oneao",
- "tencent102826474",
- "wxdab3e21a1f7e392f"
- ],
+ "scheme": ["oneao", "tencent102826474", "wxdab3e21a1f7e392f"],
"extra": {
"linkingScheme": "oneao"
},
diff --git a/code/app/app-rn/assets/expo.icon/icon.json b/code/app/app-rn/assets/expo.icon/icon.json
index 7a2c33cd..9a26f70e 100644
--- a/code/app/app-rn/assets/expo.icon/icon.json
+++ b/code/app/app-rn/assets/expo.icon/icon.json
@@ -1,40 +1,35 @@
{
- "fill" : {
- "automatic-gradient" : "extended-srgb:0.00000,0.47843,1.00000,1.00000"
+ "fill": {
+ "automatic-gradient": "extended-srgb:0.00000,0.47843,1.00000,1.00000"
},
- "groups" : [
+ "groups": [
{
- "layers" : [
+ "layers": [
{
- "image-name" : "expo-symbol 2.svg",
- "name" : "expo-symbol 2",
- "position" : {
- "scale" : 1,
- "translation-in-points" : [
- 1.1008400065293245e-05,
- -16.046875
- ]
+ "image-name": "expo-symbol 2.svg",
+ "name": "expo-symbol 2",
+ "position": {
+ "scale": 1,
+ "translation-in-points": [1.1008400065293245e-5, -16.046875]
}
},
{
- "image-name" : "grid.png",
- "name" : "grid"
+ "image-name": "grid.png",
+ "name": "grid"
}
],
- "shadow" : {
- "kind" : "neutral",
- "opacity" : 0.5
+ "shadow": {
+ "kind": "neutral",
+ "opacity": 0.5
},
- "translucency" : {
- "enabled" : true,
- "value" : 0.5
+ "translucency": {
+ "enabled": true,
+ "value": 0.5
}
}
],
- "supported-platforms" : {
- "circles" : [
- "watchOS"
- ],
- "squares" : "shared"
+ "supported-platforms": {
+ "circles": ["watchOS"],
+ "squares": "shared"
}
-}
\ No newline at end of file
+}
diff --git a/code/app/app-rn/babel.config.js b/code/app/app-rn/babel.config.js
index 9d89e131..73ebf58e 100644
--- a/code/app/app-rn/babel.config.js
+++ b/code/app/app-rn/babel.config.js
@@ -1,6 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
- presets: ['babel-preset-expo'],
+ presets: ["babel-preset-expo"],
};
};
diff --git a/code/app/app-rn/package.json b/code/app/app-rn/package.json
index a5b49d5d..f2afd93f 100644
--- a/code/app/app-rn/package.json
+++ b/code/app/app-rn/package.json
@@ -10,12 +10,14 @@
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint",
- "build": "expo prebuild"
+ "build": "expo prebuild",
+ "fmt": "oxfmt",
+ "fmt:check": "oxfmt --check"
},
"dependencies": {
"@alova/adapter-axios": "^2.0.18",
"@expo/vector-icons": "^15.1.1",
- "@gorhom/bottom-sheet": "^5.2.9",
+ "@gorhom/bottom-sheet": "^5.2.14",
"@legendapp/list": "^2.0.19",
"@react-native-async-storage/async-storage": "^2.2.0",
"alova": "^3.5.1",
@@ -41,13 +43,14 @@
"expo-system-ui": "~56.0.5",
"expo-web-browser": "~56.0.5",
"expo-wechat": "^0.2.6",
- "heroui-native": "^1.0.1",
+ "heroui-native": "^1.0.2",
"lucide-react-native": "^1.8.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.85.3",
"react-native-dates-picker": "^0.2.4",
"react-native-gesture-handler": "~2.31.2",
+ "react-native-gifted-charts": "^1.4.77",
"react-native-image-crop-picker": "^0.51.1",
"react-native-keyboard-controller": "1.21.6",
"react-native-reanimated": "4.3.1",
diff --git a/code/app/app-rn/plugins/withAndroidSignature.js b/code/app/app-rn/plugins/withAndroidSignature.js
index 61af8bad..cd625494 100644
--- a/code/app/app-rn/plugins/withAndroidSignature.js
+++ b/code/app/app-rn/plugins/withAndroidSignature.js
@@ -1,39 +1,42 @@
// plugins/withAndroidSignature.js
-const fs = require('node:fs')
-const path = require('node:path')
-const { withAppBuildGradle } = require('expo/config-plugins')
+const fs = require("node:fs");
+const path = require("node:path");
+const { withAppBuildGradle } = require("expo/config-plugins");
module.exports = function withAndroidSignature(config) {
return withAppBuildGradle(config, (config) => {
- if (config.modResults.language === 'groovy') {
- config.modResults.contents = setAndroidSignature(config.modResults.contents)
+ if (config.modResults.language === "groovy") {
+ config.modResults.contents = setAndroidSignature(config.modResults.contents);
+ } else {
+ throw new Error("如果不是 groovy,则无法在 app/build.gradle 中设置 signingConfigs");
}
- else {
- throw new Error('如果不是 groovy,则无法在 app/build.gradle 中设置 signingConfigs')
- }
- return config
- })
-}
+ return config;
+ });
+};
function setAndroidSignature(appBuildGradle) {
- if (!fs.existsSync(path.resolve(__dirname, '../credentials/credentials.json'))) {
- console.warn('警告:没有设置正式版本的 Android Keystore 文件,因为 credentials.json 不存在。')
- return appBuildGradle
+ if (!fs.existsSync(path.resolve(__dirname, "../credentials/credentials.json"))) {
+ console.warn("警告:没有设置正式版本的 Android Keystore 文件,因为 credentials.json 不存在。");
+ return appBuildGradle;
}
- const info = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../credentials/credentials.json'), { encoding: 'utf8' }))
+ const info = JSON.parse(
+ fs.readFileSync(path.resolve(__dirname, "../credentials/credentials.json"), {
+ encoding: "utf8",
+ }),
+ );
// 使用正则表达式插入签名信息
let output = appBuildGradle.replace(
/(signingConfigs\s*\{)/,
`$1
release {
- storeFile file(${JSON.stringify(path.resolve(__dirname, '../credentials/oneao-android.keystore'))})
+ storeFile file(${JSON.stringify(path.resolve(__dirname, "../credentials/oneao-android.keystore"))})
storePassword ${JSON.stringify(info.android.keystore.keystorePassword)}
keyAlias ${JSON.stringify(info.android.keystore.keyAlias)}
keyPassword ${JSON.stringify(info.android.keystore.keyPassword)}
}`,
- )
+ );
// 使用正则表达式替换 signingConfig
output = output.replace(
@@ -41,7 +44,7 @@ function setAndroidSignature(appBuildGradle) {
`$1
signingConfig signingConfigs.release
`,
- )
+ );
- return output
+ return output;
}
diff --git a/code/app/app-rn/pnpm-lock.yaml b/code/app/app-rn/pnpm-lock.yaml
index 7d943f1b..1fcfbf6a 100644
--- a/code/app/app-rn/pnpm-lock.yaml
+++ b/code/app/app-rn/pnpm-lock.yaml
@@ -15,7 +15,7 @@ importers:
specifier: ^15.1.1
version: 15.1.1(expo-font@56.0.7)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
'@gorhom/bottom-sheet':
- specifier: ^5.2.9
+ specifier: ^5.2.14
version: 5.2.14(@types/react@19.2.17)(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
'@legendapp/list':
specifier: ^2.0.19
@@ -93,7 +93,7 @@ importers:
specifier: ^0.2.6
version: 0.2.6(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
heroui-native:
- specifier: ^1.0.1
+ specifier: ^1.0.2
version: 1.0.4(cd673f28fd2c716052bd866cd2bb42c8)
lucide-react-native:
specifier: ^1.8.0
@@ -113,6 +113,9 @@ importers:
react-native-gesture-handler:
specifier: ~2.31.2
version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
+ react-native-gifted-charts:
+ specifier: ^1.4.77
+ version: 1.4.77(expo-linear-gradient@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
react-native-image-crop-picker:
specifier: ^0.51.1
version: 0.51.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
@@ -2365,6 +2368,13 @@ packages:
resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
engines: {node: '>=6'}
+ gifted-charts-core@0.1.81:
+ resolution: {integrity: sha512-plgJSbKB0Lxp2KQ/Fvj1qbOhiy6wxPiZ0Av60iFHpSSu6YlJjYhwczx5w2/iJQdZYb851OFMHgN/pgTNVLv6dA==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ react-native-svg: '*'
+
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
@@ -3153,6 +3163,20 @@ packages:
react: '*'
react-native: '*'
+ react-native-gifted-charts@1.4.77:
+ resolution: {integrity: sha512-Ul4juHO0Gicng139i61AzQ3h4kLM25dzid1rU+d3d7PuHsI4UypgeChz/luaHMZaWgXkALjq6DLqaM2Y2AEhrA==}
+ peerDependencies:
+ expo-linear-gradient: '*'
+ react: '*'
+ react-native: '*'
+ react-native-linear-gradient: '*'
+ react-native-svg: '*'
+ peerDependenciesMeta:
+ expo-linear-gradient:
+ optional: true
+ react-native-linear-gradient:
+ optional: true
+
react-native-haptic-feedback@3.0.0:
resolution: {integrity: sha512-yxBQFHhXU8S0xupGtzDRToGw5yMWezUdLzxtlx2Oi9YpjPwprWZWCw8foPt0S2t84B0ZikzZYyOHiMSXIelUxg==}
peerDependencies:
@@ -6227,6 +6251,12 @@ snapshots:
getenv@2.0.0: {}
+ gifted-charts-core@0.1.81(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3)
+ react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
+
glob@13.0.6:
dependencies:
minimatch: 10.2.5
@@ -7051,6 +7081,15 @@ snapshots:
react: 19.2.3
react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3)
+ react-native-gifted-charts@1.4.77(expo-linear-gradient@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3):
+ dependencies:
+ gifted-charts-core: 0.1.81(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3)
+ react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
+ optionalDependencies:
+ expo-linear-gradient: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)
+
react-native-haptic-feedback@3.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3))(react@19.2.3):
dependencies:
react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.17)(react@19.2.3)
diff --git a/code/app/app-rn/pnpm-workspace.yaml b/code/app/app-rn/pnpm-workspace.yaml
new file mode 100644
index 00000000..bc58ec4f
--- /dev/null
+++ b/code/app/app-rn/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+allowBuilds:
+ "@shopify/react-native-skia": set this to true or false
diff --git a/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx
index 161c6196..16d64f72 100644
--- a/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx
+++ b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx
@@ -1,9 +1,15 @@
import { useCallback, useEffect, useRef, useState, memo, useMemo } from "react";
-import { Image, Text, TextInput, View, Keyboard, Animated } from "react-native";
+import { Image, Text, TextInput, View, Keyboard } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { useRouter } from "expo-router";
import { Menu, PressableFeedback, Spinner } from "heroui-native";
-import { AlertCircleIcon, PaperclipIcon, PlayIcon, SearchIcon, SendHorizonalIcon } from "lucide-react-native";
+import {
+ AlertCircleIcon,
+ PaperclipIcon,
+ PlayIcon,
+ SearchIcon,
+ SendHorizonalIcon,
+} from "lucide-react-native";
import { toast } from "sonner-native";
import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus";
@@ -42,7 +48,9 @@ function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: ()
/>
) : (
- {u.nickname?.charAt(0) || "?"}
+
+ {u.nickname?.charAt(0) || "?"}
+
)}
@@ -69,7 +77,12 @@ function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: ()
// ===================== 消息气泡 =====================
const ChatMessageItem = memo(function ChatMessageItem({
- item, isSelf, members, isAdmin, onRecall, onDelete,
+ item,
+ isSelf,
+ members,
+ isAdmin,
+ onRecall,
+ onDelete,
}: {
item: ChatMessage;
isSelf: boolean;
@@ -109,7 +122,9 @@ const ChatMessageItem = memo(function ChatMessageItem({
(canRecall || isAdmin) && setShowActions(true)}>
{!isSelf && (
- router.navigate(`/family/member/${item.user_id}` as any)}>
+ router.navigate(`/family/member/${item.user_id}` as any)}
+ >
{member?.avatar ? (
-
+
{item.content}
@@ -173,17 +193,30 @@ const ChatMessageItem = memo(function ChatMessageItem({
isSelf ? "bg-accent rounded-br-md" : "bg-white rounded-bl-md shadow-sm",
)}
>
-
+
{item.attachments[0].file_name}
-
+
{item.attachments[0].file_size
? (item.attachments[0].file_size / 1024).toFixed(0) + " KB"
: ""}
@@ -201,10 +234,19 @@ const ChatMessageItem = memo(function ChatMessageItem({
try {
const d = JSON.parse(item.source_data);
return Object.entries(d)
- .filter(([k]) =>
- !["id", "create_time", "update_time", "create_by", "update_by", "family_id"].includes(k))
+ .filter(
+ ([k]) =>
+ ![
+ "id",
+ "create_time",
+ "update_time",
+ "create_by",
+ "update_by",
+ "family_id",
+ ].includes(k),
+ )
.map(([k, v]) => `${k}: ${v}`)
- .join(" ");
+ .join(" ");
} catch {
return item.source_data;
}
@@ -271,22 +313,30 @@ export default function ChatPage() {
const { loading, loadingMore, runInitial, runLoadMore } = useListLoading();
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
- const [members, setMembers] = useState>({});
+ const [members, setMembers] = useState<
+ Record
+ >({});
const [inputText, setInputText] = useState("");
const [adminRole, setAdminRole] = useState(0);
const listRef = useRef(null);
- const opacity = useRef(new Animated.Value(0)).current;
- const revealed = useRef(false);
// ---- optimistic send ----
- type PendingMsg = { localId: string; content: string; status: "sending" | "failed"; createTime: string };
+ type PendingMsg = {
+ localId: string;
+ content: string;
+ status: "sending" | "failed";
+ createTime: string;
+ };
const [pending, setPending] = useState([]);
const pendingTimers = useRef>>({});
const removePending = useCallback((localId: string) => {
setPending((prev) => prev.filter((p) => p.localId !== localId));
const t = pendingTimers.current[localId];
- if (t) { clearTimeout(t); delete pendingTimers.current[localId]; }
+ if (t) {
+ clearTimeout(t);
+ delete pendingTimers.current[localId];
+ }
}, []);
// Watch ws.messages — when a new message from self appears, resolve matching pending
@@ -354,12 +404,15 @@ export default function ChatPage() {
const ids = new Set(history.map((m) => m.id));
const merged: (ChatMessage & { _pending?: PendingMsg })[] = [...history];
for (const m of ws.messages) {
- if (!ids.has(m.id)) { merged.push(m); ids.add(m.id); }
+ if (!ids.has(m.id)) {
+ merged.push(m);
+ ids.add(m.id);
+ }
}
// Append pending (sending/failed) at the end
for (const p of pending) {
merged.push({
- id: -(Date.now() % 100000), // negative ID for local-only items
+ id: -(Date.now() % 100000), // negative ID for local-only items
family_id: familyId ?? 0,
user_id: userId,
nickname: "",
@@ -380,37 +433,31 @@ export default function ChatPage() {
}, [history, ws.messages, pending, familyId, userId]);
// Non-inverted: scrollToEnd = latest messages
- const scrollToBottom = useCallback(
- (animated = true) => {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- listRef.current?.scrollToEnd?.({ animated });
- });
- });
- },
- [],
- );
-
- const revealAtBottom = useCallback(() => {
+ const scrollToBottom = useCallback((animated = true) => {
requestAnimationFrame(() => {
- listRef.current?.scrollToEnd?.({ animated: false });
requestAnimationFrame(() => {
- listRef.current?.scrollToEnd?.({ animated: false });
- opacity.setValue(1);
+ listRef.current?.scrollToEnd?.({ animated });
});
});
- }, [opacity]);
+ }, []);
- // Keyboard appears → double-tap scroll: first to catch the new viewport,
- // second as a safety net after FlatList internal measurements settle.
+ // Keyboard appears → 等键盘动画完成再滚到底
useEffect(() => {
const sub = Keyboard.addListener("keyboardDidShow", () => {
- setTimeout(() => scrollToBottom(true), 200);
- setTimeout(() => scrollToBottom(true), 400);
+ setTimeout(() => scrollToBottom(true), 150);
+ setTimeout(() => scrollToBottom(true), 350);
});
return () => sub.remove();
}, [scrollToBottom]);
+ // 首次加载完成后滚到底部
+ const initialScrollDone = useRef(false);
+ useEffect(() => {
+ if (loading || initialScrollDone.current || allMessages.length === 0) return;
+ initialScrollDone.current = true;
+ scrollToBottom(true);
+ }, [loading, allMessages.length, scrollToBottom]);
+
// 成员映射
useEffect(() => {
if (!familyId) return;
@@ -511,27 +558,34 @@ export default function ChatPage() {
// 10-second timeout → mark as failed
pendingTimers.current[localId] = setTimeout(() => {
- setPending((prev) => prev.map((p) => (p.localId === localId ? { ...p, status: "failed" } : p)));
+ setPending((prev) =>
+ prev.map((p) => (p.localId === localId ? { ...p, status: "failed" } : p)),
+ );
toast.error("发送失败", { id: localId });
}, 10_000);
}
// Retry a failed message
function handleRetry(p: PendingMsg) {
- setPending((prev) => prev.map((x) => (x.localId === p.localId ? { ...x, status: "sending" } : x)));
+ setPending((prev) =>
+ prev.map((x) => (x.localId === p.localId ? { ...x, status: "sending" } : x)),
+ );
toast.loading("正在发送", { id: p.localId });
ws.send({ type: "send", content: p.content });
pendingTimers.current[p.localId] = setTimeout(() => {
- setPending((prev) => prev.map((x) => (x.localId === p.localId ? { ...x, status: "failed" } : x)));
+ setPending((prev) =>
+ prev.map((x) => (x.localId === p.localId ? { ...x, status: "failed" } : x)),
+ );
toast.error("发送失败", { id: p.localId });
}, 10_000);
}
+ // 数据加载完成后,标记 listReady=true,让 FlatList 开始渲染
// Auto-scroll when a new message arrives (WebSocket push)
const msgCountRef = useRef(0);
useEffect(() => {
if (msgCountRef.current === 0 && allMessages.length > 0) {
- // Initial data just arrived — seed count, don't scroll
+ // 首次有数据,只记录数量不滚动(由 initialScrollDone effect 处理)
msgCountRef.current = allMessages.length;
return;
}
@@ -550,121 +604,110 @@ export default function ChatPage() {
}
return (
-
-
+
+
router.navigate("/chat/history" as any)}
- />
- }
- footer={
-
-
- scrollToBottom(true)}
- onSubmitEditing={handleSend}
- returnKeyType="send"
- multiline
- maxLength={800}
- textAlignVertical="center"
- />
-
-
-
-
-
-
-
-
- }
- data={allMessages}
- keyExtractor={(item) => String(item.id)}
- contentContainerStyle={{ paddingBottom: 12 }}
- onScroll={({ nativeEvent }) => {
- if (nativeEvent.contentOffset.y < 30 && hasMore && !loadingMore) {
- handleLoadMore();
- }
- }}
- scrollEventThrottle={200}
- onContentSizeChange={() => {
- if (!revealed.current) {
- revealed.current = true;
- revealAtBottom();
- return;
- }
- if (!loadingMore) scrollToBottom(false);
- }}
- ListHeaderComponent={
- loadingMore ? (
-
-
-
- ) : null
- }
- ListEmptyComponent={
-
- 暂无消息,开始聊天吧
-
- }
- renderItem={({ item }: { item: any }) => {
- const pending = item._pending as PendingMsg | undefined;
- if (pending) {
- return (
-
- {pending.status === "sending" ? (
-
-
-
- ) : (
- handleRetry(pending)}>
-
-
-
-
-
- )}
-
-
- {pending.content}
-
-
-
- );
- }
- return (
- router.navigate("/chat/history" as any)}
/>
- );
- }}
- />
-
-
+ }
+ footer={
+
+
+ setTimeout(() => scrollToBottom(true), 250)}
+ onSubmitEditing={handleSend}
+ returnKeyType="send"
+ multiline
+ maxLength={800}
+ textAlignVertical="center"
+ />
+
+
+
+
+
+
+
+
+ }
+ data={allMessages}
+ keyExtractor={(item) => String(item.id)}
+ contentContainerStyle={{ paddingBottom: 12 }}
+ onScroll={({ nativeEvent }) => {
+ if (nativeEvent.contentOffset.y < 30 && hasMore && !loadingMore) {
+ handleLoadMore();
+ }
+ }}
+ scrollEventThrottle={200}
+ onContentSizeChange={() => {
+ if (!loadingMore) scrollToBottom(false);
+ }}
+ ListHeaderComponent={
+ loadingMore ? (
+
+
+
+ ) : null
+ }
+ ListEmptyComponent={
+
+ 暂无消息,开始聊天吧
+
+ }
+ renderItem={({ item }: { item: any }) => {
+ const pending = item._pending as PendingMsg | undefined;
+ if (pending) {
+ return (
+
+ {pending.status === "sending" ? (
+
+
+
+ ) : (
+ handleRetry(pending)}>
+
+
+
+
+
+ )}
+
+
+ {pending.content}
+
+
+
+ );
+ }
+ return (
+
+ );
+ }}
+ />
+
+
);
}
diff --git a/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx b/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx
index 2fc409dd..9393be24 100644
--- a/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx
+++ b/code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx
@@ -20,9 +20,7 @@ function SectionLabel({
}) {
return (
-
- {title}
-
+ {title}
{more ? (
{more}
@@ -45,14 +43,9 @@ function GridItem({
onPress: () => void;
}) {
return (
-
+
-
- {title}
-
+ {title}
);
@@ -112,11 +105,7 @@ export default function Home() {
onPress={() => router.navigate(app.id as any)}
/>
))}
- router.navigate("/tools")}
- />
+ router.navigate("/tools")} />
{/* 填充空位保证对齐 */}
{Array.from({
length: Math.max(0, 2 - SUB_APPS.length),
diff --git a/code/app/app-rn/src/app/(main)/(tabs)/space/index.tsx b/code/app/app-rn/src/app/(main)/(tabs)/space/index.tsx
index 189975f2..93eb90a9 100644
--- a/code/app/app-rn/src/app/(main)/(tabs)/space/index.tsx
+++ b/code/app/app-rn/src/app/(main)/(tabs)/space/index.tsx
@@ -1,10 +1,49 @@
-import LayoutView from "@/components/layout/view";
-import EmptyState from "@/components/EmptyStatus";
+import Ionicons from "@expo/vector-icons/Ionicons";
+import BottomSheet from "@/components/BottomSheet";
+import { Button } from "heroui-native";
+import { useState } from "react";
+import { Text, View } from "react-native";
+import { withUniwind } from "uniwind";
+
+const StyledIonicons = withUniwind(Ionicons);
+
+export default function BottomSheetExample() {
+ const [isOpen, setIsOpen] = useState(false);
-export default function Space() {
return (
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keep yourself safe
+
+
+ Update your software to the latest version for better security and performance.
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/code/app/app-rn/src/app/(main)/auth/login.tsx b/code/app/app-rn/src/app/(main)/auth/login.tsx
index 6be1e693..c9e2ce06 100644
--- a/code/app/app-rn/src/app/(main)/auth/login.tsx
+++ b/code/app/app-rn/src/app/(main)/auth/login.tsx
@@ -76,9 +76,7 @@ export default function Login() {
}
if (!onLoginFinished.success) {
- toast.error(
- onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权"
- );
+ toast.error(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
stopLoading();
return;
}
@@ -87,9 +85,7 @@ export default function Login() {
try {
const tokenInfo = await ExpoQQ.getLoginTokenInfo();
if (!tokenInfo.openId) {
- toast.error(
- tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权"
- );
+ toast.error(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
stopLoading();
return;
}
@@ -255,9 +251,7 @@ export default function Login() {
className="w-20 h-20 mb-4"
resizeMode="contain"
/>
-
- Welcome to Allapp
-
+ Welcome to Allapp
diff --git a/code/app/app-rn/src/app/(main)/chat/history.tsx b/code/app/app-rn/src/app/(main)/chat/history.tsx
index c70012df..f6b76bd2 100644
--- a/code/app/app-rn/src/app/(main)/chat/history.tsx
+++ b/code/app/app-rn/src/app/(main)/chat/history.tsx
@@ -6,7 +6,8 @@ import { cn } from "@/utils/cn";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
import { useRouter } from "expo-router";
-import { BottomSheet, Button, Label, SearchField, Spinner, PressableFeedback } from "heroui-native";
+import { Button, Label, SearchField, Spinner, PressableFeedback } from "heroui-native";
+import BottomSheet from "@/components/BottomSheet";
import { CalendarDaysIcon, CheckIcon, FilterIcon, RotateCcwIcon } from "lucide-react-native";
import dayjs from "dayjs";
import { ChatMsgType, normalizeChatMessage } from "@/types";
@@ -52,7 +53,7 @@ export default function ChatHistoryPage() {
const [memberMap, setMemberMap] = useState>({});
const familyId = useFamilyStore((s) => s.getFamilyId());
- // 加载成员昵称映射
+ // 鍔犺浇鎴愬憳鏄电О鏄犲皠
useEffect(() => {
if (!familyId) return;
@@ -201,7 +202,7 @@ export default function ChatHistoryPage() {
>
- 筛选{activeFilterCount ? ` ${activeFilterCount}` : ""}
+ 筛选{activeFilterCount > 0 ? ` (${activeFilterCount})` : ""}
@@ -225,9 +226,7 @@ export default function ChatHistoryPage() {
router.navigate(`/family/member/${item.user_id}` as any)}
>
-
- {memberMap[item.user_id] || "未知"}
-
+ {memberMap[item.user_id] || "未知"}
{formatTime(item.create_time)}
@@ -260,7 +259,7 @@ export default function ChatHistoryPage() {
-
+
筛选聊天记录
@@ -291,7 +290,7 @@ export default function ChatHistoryPage() {
-
+
{MSG_TYPES.map((t) => (
setDraftTypeFilter(t.value)}>
@@ -301,7 +300,12 @@ export default function ChatHistoryPage() {
draftTypeFilter === t.value ? "bg-green-500" : "bg-gray-200",
)}
>
-
+
{t.label}
diff --git a/code/app/app-rn/src/app/(main)/family/entry.tsx b/code/app/app-rn/src/app/(main)/family/entry.tsx
index ba88bfd4..139c31d9 100644
--- a/code/app/app-rn/src/app/(main)/family/entry.tsx
+++ b/code/app/app-rn/src/app/(main)/family/entry.tsx
@@ -2,12 +2,7 @@ import { useState, useRef, useEffect } from "react";
import { TouchableOpacity, Text, View, Keyboard } from "react-native";
import { Stack } from "expo-router";
import EmptyState from "@/components/EmptyStatus";
-import {
- Button,
- Input,
- InputOTP,
- type InputOTPRef,
-} from "heroui-native";
+import { Button, Input, InputOTP, type InputOTPRef } from "heroui-native";
import { PlusIcon, HousePlusIcon } from "lucide-react-native";
import { getUniqueIdApi, loadDataApi, saveDataApi } from "@/request/api";
import { useUserStore, useFamilyStore } from "@/store";
@@ -195,13 +190,7 @@ export default function EntryFamily() {
view_name: "b_finance_category_default",
search_condition: "",
order_by: "",
- search_columns: [
- "name",
- "type",
- "icon_file_key",
- "icon_mime",
- "sort_number",
- ],
+ search_columns: ["name", "type", "icon_file_key", "icon_mime", "sort_number"],
args: [],
});
@@ -339,11 +328,7 @@ export default function EntryFamily() {
}}
/>
-
+
@@ -397,7 +382,11 @@ export default function EntryFamily() {
className="h-12"
/>
-
-
);
}
diff --git a/code/app/app-rn/src/app/_layout.tsx b/code/app/app-rn/src/app/_layout.tsx
index 52886f08..ce46f715 100644
--- a/code/app/app-rn/src/app/_layout.tsx
+++ b/code/app/app-rn/src/app/_layout.tsx
@@ -1,4 +1,5 @@
import { DefaultTheme, ThemeProvider } from "expo-router";
+import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { StatusBar } from "expo-status-bar";
import { HeroUINativeProvider } from "heroui-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -25,21 +26,23 @@ export default function RootLayout() {
if (!isReady) return null;
return (
-
+
-
-
-
+
+
+
+
+
diff --git a/code/app/app-rn/src/app/finance/(tabs)/_layout.tsx b/code/app/app-rn/src/app/finance/(tabs)/_layout.tsx
index 868010a4..23bdbbb9 100644
--- a/code/app/app-rn/src/app/finance/(tabs)/_layout.tsx
+++ b/code/app/app-rn/src/app/finance/(tabs)/_layout.tsx
@@ -4,11 +4,7 @@ import ModuleLayout from "@/layouts/ModuleLayout";
import { TabbarItem } from "@/components/layout/tabbar";
import { useFinanceStore } from "@/store/finance";
import { useFamilyStore } from "@/store/family";
-import {
- ChartNoAxesColumnIcon,
- CircleDollarSignIcon,
- PlusIcon,
-} from "lucide-react-native";
+import { ChartNoAxesColumnIcon, CircleDollarSignIcon, PlusIcon } from "lucide-react-native";
const tabs: TabbarItem[] = [
{
diff --git a/code/app/app-rn/src/app/finance/(tabs)/analysis/index.tsx b/code/app/app-rn/src/app/finance/(tabs)/analysis/index.tsx
index 9cf0b83a..e79ab54f 100644
--- a/code/app/app-rn/src/app/finance/(tabs)/analysis/index.tsx
+++ b/code/app/app-rn/src/app/finance/(tabs)/analysis/index.tsx
@@ -1,8 +1,22 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
-import { CartesianChart, Bar, Line } from "victory-native";
import dayjs from "dayjs";
-import { CalendarDaysIcon, RotateCcwIcon } from "lucide-react-native";
+
+/* ============== 饼图颜色 ============== */
+const PIE_COLORS = [
+ "#FB7185",
+ "#F472B6",
+ "#A78BFA",
+ "#818CF8",
+ "#34D399",
+ "#2DD4BF",
+ "#22D3EE",
+ "#38BDF8",
+];
+import { CalendarDaysIcon, CheckIcon, FilterIcon, RotateCcwIcon, XIcon } from "lucide-react-native";
+
+import { Button, Label, TextField } from "heroui-native";
+import BottomSheet from "@/components/BottomSheet";
import DateTimeRange from "@/components/DateTimeRange";
import EmptyState from "@/components/EmptyStatus";
@@ -14,9 +28,15 @@ import { useFamilyStore } from "@/store";
import { FinanceBill, FinanceType } from "@/types/finance";
import { getEndOfMonth, getStartOfMonth } from "@/utils/time";
+/* ============== 类型 ============== */
+
interface FilterState {
startDate: string;
endDate: string;
+ categoryId: number | null;
+ memberId: number | null;
+ type: "all" | FinanceType;
+ keyword: string;
}
interface TrendDatum {
@@ -37,23 +57,35 @@ interface CategoryStat {
percent: number;
}
-interface MemberStat {
- memberId: number;
- nickname: string;
- expense: number;
- income: number;
- count: number;
+interface CategoryOption {
+ id: number;
+ name: string;
+ icon_file_key?: string;
+ type: FinanceType;
}
+interface MemberOption {
+ id: number;
+ nickname: string;
+}
+
+/* ============== 常量 ============== */
+
const moneyFormatter = new Intl.NumberFormat("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
+/* ============== 工具函数 ============== */
+
function createDefaultFilter(): FilterState {
return {
startDate: getStartOfMonth(),
endDate: getEndOfMonth(),
+ categoryId: null,
+ memberId: null,
+ type: "all",
+ keyword: "",
};
}
@@ -82,18 +114,47 @@ function getDateRange(startDate: string, endDate: string) {
return days;
}
-function getTypeLabel(type: FinanceType) {
- return type === FinanceType.Income ? "收入" : "支出";
-}
+/* ============== 页面组件 ============== */
export default function FinanceAnalysis() {
const familyId = useFamilyStore((s) => s.getFamilyId());
const { loading, runInitial } = useListLoading();
const [filter, setFilter] = useState(() => createDefaultFilter());
+ const [draftFilter, setDraftFilter] = useState(() => createDefaultFilter());
+ const [showFilterSheet, setShowFilterSheet] = useState(false);
const [showDateRange, setShowDateRange] = useState(false);
const [bills, setBills] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [members, setMembers] = useState([]);
+ /* —— 加载筛选选项 —— */
+ const loadFilterOptions = useCallback(async (currentFamilyId: number) => {
+ const categorySQL =
+ "select id, name, type, icon_file_key\n" +
+ "from b_finance_category\n" +
+ "where family_id = ? and status = 1\n" +
+ "order by type asc, sort_number asc, id asc";
+ const memberSQL =
+ "select m.user_id as id, coalesce(m.nickname, u.name) as nickname\n" +
+ "from b_family_member m\n" +
+ "left join b_user u on u.id = m.user_id\n" +
+ "where m.family_id = ?\n" +
+ "order by m.id asc";
+ const [catRes, memRes] = await Promise.all([
+ loadDataBySqlApi({ sql: categorySQL, args: [currentFamilyId] }),
+ loadDataBySqlApi({ sql: memberSQL, args: [currentFamilyId] }),
+ ]);
+
+ if (catRes.isSuccess) {
+ setCategories((catRes.data ?? []) as CategoryOption[]);
+ }
+ if (memRes.isSuccess) {
+ setMembers((memRes.data ?? []) as MemberOption[]);
+ }
+ }, []);
+
+ /* —— 加载账单 —— */
const loadBills = useCallback(
async (nextFilter = filter) => {
if (!familyId) {
@@ -101,40 +162,62 @@ export default function FinanceAnalysis() {
return;
}
- const res = await loadDataBySqlApi({
- sql: `
- select
- f.id,
- f.family_id,
- f.category_id,
- f.amount,
- f.remark,
- f.record_time,
- f.create_by,
- f.create_time,
- f.update_by,
- f.update_time,
- c.name as category_name,
- c.type as category_type,
- c.icon_file_key as category_icon_file_key,
- coalesce(m.nickname, u.name) as member_nickname
- from b_finance f
- left join b_finance_category c on c.id = f.category_id
- left join b_family_member m on m.family_id = f.family_id and m.user_id = f.create_by
- left join b_user u on u.id = f.create_by
- where f.family_id = ?
- and f.record_time >= ?
- and f.record_time < ?
- order by f.record_time asc, f.create_time asc, f.id asc
- `,
- args: [
- familyId,
- nextFilter.startDate,
- dayjs(nextFilter.endDate).add(1, "day").format("YYYY-MM-DD"),
- ],
- });
+ const conditions = ["f.family_id = ?", "f.record_time >= ?", "f.record_time < ?"];
+ const args: any[] = [
+ familyId,
+ nextFilter.startDate,
+ dayjs(nextFilter.endDate).add(1, "day").format("YYYY-MM-DD"),
+ ];
+
+ if (nextFilter.categoryId) {
+ conditions.push("f.category_id = ?");
+ args.push(nextFilter.categoryId);
+ }
+
+ if (nextFilter.memberId) {
+ conditions.push("f.create_by = ?");
+ args.push(nextFilter.memberId);
+ }
+
+ if (nextFilter.type !== "all") {
+ conditions.push("c.type = ?");
+ args.push(nextFilter.type);
+ }
+
+ const keyword = nextFilter.keyword.trim();
+ if (keyword) {
+ conditions.push("coalesce(f.remark, '') like ?");
+ args.push(`%${keyword}%`);
+ }
+
+ const querySQL =
+ "select\n" +
+ " f.id,\n" +
+ " f.family_id,\n" +
+ " f.category_id,\n" +
+ " f.amount,\n" +
+ " f.remark,\n" +
+ " f.record_time,\n" +
+ " f.create_by,\n" +
+ " f.create_time,\n" +
+ " f.update_by,\n" +
+ " f.update_time,\n" +
+ " c.name as category_name,\n" +
+ " c.type as category_type,\n" +
+ " c.icon_file_key as category_icon_file_key,\n" +
+ " coalesce(m.nickname, u.name) as member_nickname\n" +
+ "from b_finance f\n" +
+ "left join b_finance_category c on c.id = f.category_id\n" +
+ "left join b_family_member m on m.family_id = f.family_id and m.user_id = f.create_by\n" +
+ "left join b_user u on u.id = f.create_by\n" +
+ "where " +
+ conditions.join(" and ") +
+ "\n" +
+ "order by f.record_time asc, f.create_time asc, f.id asc";
+ const res = await loadDataBySqlApi({ sql: querySQL, args });
if (res.isSuccess) {
+ console.log(res.data);
setBills((res.data ?? []) as FinanceBill[]);
}
},
@@ -142,9 +225,12 @@ export default function FinanceAnalysis() {
);
useEffect(() => {
+ if (!familyId) return;
+ loadFilterOptions(familyId);
runInitial(() => loadBills(filter));
}, [familyId]);
+ /* —— 计算属性 —— */
const totals = useMemo(
() =>
bills.reduce(
@@ -219,50 +305,31 @@ export default function FinanceAnalysis() {
map.set(key, stat);
});
+ const total = totals.expense + totals.income;
+
return Array.from(map.values())
.map((item) => ({
...item,
- percent:
- item.type === FinanceType.Income
- ? totals.income > 0
- ? item.amount / totals.income
- : 0
- : totals.expense > 0
- ? item.amount / totals.expense
- : 0,
+ percent: total > 0 ? item.amount / total : 0,
}))
.sort((a, b) => b.amount - a.amount)
.slice(0, 8);
}, [bills, totals.expense, totals.income]);
- const memberStats = useMemo(() => {
- const map = new Map();
-
- bills.forEach((item) => {
- const memberId = Number(item.create_by) || 0;
- const stat = map.get(memberId) ?? {
- memberId,
- nickname: item.member_nickname || "未知成员",
- expense: 0,
- income: 0,
- count: 0,
- };
- const amount = Number(item.amount) || 0;
-
- if (item.category_type === FinanceType.Income) {
- stat.income += amount;
- } else {
- stat.expense += amount;
- }
-
- stat.count += 1;
- map.set(memberId, stat);
- });
-
- return Array.from(map.values())
- .sort((a, b) => b.expense + b.income - (a.expense + a.income))
- .slice(0, 6);
- }, [bills]);
+ /* —— 柱状图数据 —— */
+ const barData = useMemo(
+ () =>
+ trendData
+ .filter((d) => d.expense > 0 || d.income > 0)
+ .map((d) => ({
+ label: d.label,
+ stacks: [
+ { value: d.expense, color: "#FB7185" },
+ { value: d.income, color: "#10B981" },
+ ],
+ })),
+ [trendData],
+ );
const maxTrendValue = useMemo(
() =>
@@ -273,18 +340,87 @@ export default function FinanceAnalysis() {
[trendData],
);
- const handleApplyDateRange = useCallback(
- (startDate: string, endDate: string) => {
- const nextFilter = {
- startDate,
- endDate: endDate || startDate,
- };
- setFilter(nextFilter);
- setShowDateRange(false);
- runInitial(() => loadBills(nextFilter));
- },
- [loadBills, runInitial],
- );
+ const trendSummary = useMemo(() => {
+ let maxExpense = 0;
+ let maxIncome = 0;
+ let maxExpenseDay = "";
+ let maxIncomeDay = "";
+ let totalExpense = 0;
+ let totalIncome = 0;
+ let nonZeroDays = 0;
+
+ trendData.forEach((item) => {
+ const expense = Number(item.expense) || 0;
+ const income = Number(item.income) || 0;
+ totalExpense += expense;
+ totalIncome += income;
+ if (expense > 0 || income > 0) nonZeroDays += 1;
+ if (expense > maxExpense) {
+ maxExpense = expense;
+ maxExpenseDay = item.day;
+ }
+ if (income > maxIncome) {
+ maxIncome = income;
+ maxIncomeDay = item.day;
+ }
+ });
+
+ return {
+ maxExpense,
+ maxIncome,
+ maxExpenseDay,
+ maxIncomeDay,
+ totalExpense,
+ totalIncome,
+ nonZeroDays,
+ };
+ }, [trendData]);
+
+ const activeFilterCount = useMemo(() => {
+ let count = 0;
+ if (filter.categoryId) count += 1;
+ if (filter.memberId) count += 1;
+ if (filter.type !== "all") count += 1;
+ if (filter.keyword.trim()) count += 1;
+ if (filter.startDate !== getStartOfMonth() || filter.endDate !== getEndOfMonth()) count += 1;
+ return count;
+ }, [filter]);
+
+ /* 活跃筛选标签 */
+ const activeFilterLabels = useMemo(() => {
+ const labels: { key: string; text: string }[] = [];
+ if (filter.categoryId) {
+ const cat = categories.find((c) => c.id === filter.categoryId);
+ labels.push({ key: "cat", text: `分类:{cat?.name || String(filter.categoryId)}` });
+ }
+ if (filter.memberId) {
+ const mem = members.find((m) => m.id === filter.memberId);
+ labels.push({ key: "mem", text: `成员:{mem?.nickname || String(filter.memberId)}` });
+ }
+ if (filter.type !== "all") {
+ labels.push({ key: "type", text: filter.type === FinanceType.Expense ? "仅支出" : "仅收入" });
+ }
+ if (filter.keyword.trim()) {
+ labels.push({ key: "kw", text: `备注:{filter.keyword.trim()}` });
+ }
+ return labels;
+ }, [filter, categories, members]);
+
+ /* —— 交互回调 —— */
+ const openFilterSheet = useCallback(() => {
+ setDraftFilter(filter);
+ setShowFilterSheet(true);
+ }, [filter]);
+
+ const handleApplyFilter = useCallback(() => {
+ setFilter(draftFilter);
+ setShowFilterSheet(false);
+ runInitial(() => loadBills(draftFilter));
+ }, [draftFilter, loadBills, runInitial]);
+
+ const handleResetFilter = useCallback(() => {
+ setDraftFilter(createDefaultFilter());
+ }, []);
const handleReset = useCallback(() => {
const nextFilter = createDefaultFilter();
@@ -300,38 +436,61 @@ export default function FinanceAnalysis() {
-
- 分析范围
-
- {filter.startDate} 至 {filter.endDate}
-
-
+
+ {filter.startDate} 至 {filter.endDate}
+
runInitial(() => loadBills(filter))}
>
setShowDateRange(true)}
+ onPress={openFilterSheet}
>
-
- 日期
+
+
+ 筛选{activeFilterCount > 0 ? ` ${activeFilterCount}` : ""}
+
+ {activeFilterLabels.length > 0 && (
+
+ {activeFilterLabels.map((label) => (
+
+
+ {label.text}
+
+
+ ))}
+
+ )}
+
-
-
+
+
- = 0 ? "income" : "expense"} />
+ = 0 ? "income" : "expense"}
+ />
@@ -342,81 +501,335 @@ export default function FinanceAnalysis() {
) : (
<>
-
-
-
- data={trendData}
- xKey="label"
- yKeys={["expense", "income"]}
- domain={{ y: [0, maxTrendValue] }}
- domainPadding={{ left: 18, right: 18, top: 16 }}
- axisOptions={{
- lineColor: "#E5E7EB",
- labelColor: "#6B7280",
- tickCount: 4,
- }}
- >
- {({ points, chartBounds }) => (
- <>
-
-
- >
- )}
-
+ {/* —— 收支对比(横向柱状图)—— */}
+
+
+ {barData.length > 0 ? (
+
+ ) : (
+
+ 暂无收支记录
+
+ )}
-
-
-
+
+
+
+
+ 支出峰值
+
+
+ ¥{formatMoney(trendSummary.maxExpense)}
+
+
+ {trendSummary.maxExpenseDay
+ ? dayjs(trendSummary.maxExpenseDay).format("MM/DD")
+ : "—"}
+
+
+
+
+
+ 收入峰值
+
+
+ ¥{formatMoney(trendSummary.maxIncome)}
+
+
+ {trendSummary.maxIncomeDay
+ ? dayjs(trendSummary.maxIncomeDay).format("MM/DD")
+ : "—"}
+
+
+
+ 日均支出
+
+ ¥
+ {formatMoney(
+ trendSummary.nonZeroDays > 0
+ ? trendSummary.totalExpense / trendData.length
+ : 0,
+ )}
+
+ 共 {trendData.length} 天
+
+
+
+
+
-
-
- {categoryStats.map((item, index) => (
-
- ))}
-
-
-
-
-
- {memberStats.map((item, index) => (
-
- ))}
+ {/* —— 分类占比 —— */}
+
+
+ {(() => {
+ const valid = categoryStats.filter((d) => d.amount > 0);
+ const pieTotal = valid.reduce((s, d) => s + d.amount, 0);
+ if (pieTotal <= 0) {
+ return (
+
+ 暂无分类数据
+
+ );
+ }
+ return (
+
+ {valid.map((item, i) => {
+ const pct = (item.amount / pieTotal) * 100;
+ const color = PIE_COLORS[i % PIE_COLORS.length];
+ return (
+
+ {/* 左侧:分类图标 */}
+
+ {/* 右侧:内容 */}
+
+
+ {item.categoryName}
+
+ ¥{formatCompactMoney(item.amount)} ({Math.round(pct)}%)
+
+
+
+
+
+
+
+ );
+ })}
+
+ );
+ })()}
>
)}
+ {/* —— 筛选 BottomSheet —— */}
+
+
+
+
+
+
+ 筛选分析
+
+
+ 重置
+
+
+
+
+
+ setShowDateRange(true)}
+ >
+
+ {draftFilter.startDate} 至 {draftFilter.endDate || draftFilter.startDate}
+
+
+
+
+
+
+
+
+ {[
+ { key: "all" as const, label: "全部" },
+ { key: FinanceType.Expense, label: "支出" },
+ { key: FinanceType.Income, label: "收入" },
+ ].map((option) => {
+ const selected = draftFilter.type === option.key;
+ return (
+
+ setDraftFilter((prev) => ({
+ ...prev,
+ type: option.key as FilterState["type"],
+ }))
+ }
+ >
+
+ {option.label}
+
+
+ );
+ })}
+
+
+
+
+
+
+ setDraftFilter((prev) => ({ ...prev, memberId: null }))}
+ >
+
+ 全部
+
+
+ {members.map((member) => {
+ const selected = draftFilter.memberId === member.id;
+ return (
+
+ setDraftFilter((prev) => ({
+ ...prev,
+ memberId: prev.memberId === member.id ? null : member.id,
+ }))
+ }
+ >
+
+ {member.nickname}
+
+
+ );
+ })}
+
+
+
+
+
+
+ setDraftFilter((prev) => ({ ...prev, categoryId: null }))}
+ >
+
+ 全部
+
+
+ {categories.map((cat) => {
+ const selected = draftFilter.categoryId === cat.id;
+ return (
+
+ setDraftFilter((prev) => ({
+ ...prev,
+ categoryId: prev.categoryId === cat.id ? null : cat.id,
+ }))
+ }
+ >
+
+
+ {cat.name}
+
+
+ );
+ })}
+
+
+
+
+
+ setDraftFilter((prev) => ({ ...prev, keyword }))}
+ placeholder="按备注模糊搜索"
+ returnKeyType="search"
+ />
+
+
+
+ setShowFilterSheet(false)}
+ >
+
+ 取消
+
+
+
+ 应用
+
+
+
+
+
+
+
setShowDateRange(false)}
- onConfirm={handleApplyDateRange}
+ onConfirm={(startDate, endDate) => {
+ setDraftFilter((prev) => ({
+ ...prev,
+ startDate,
+ endDate: endDate || startDate,
+ }));
+ setShowDateRange(false);
+ }}
/>
);
}
+/* ============== 子组件 ============== */
+
function SummaryCard({
label,
value,
@@ -468,60 +881,45 @@ function LegendDot({ color, label }: { color: string; label: string }) {
);
}
-function CategoryRow({ item, isLast }: { item: CategoryStat; isLast: boolean }) {
- const isIncome = item.type === FinanceType.Income;
- const percentText = `${Math.round(item.percent * 100)}%`;
+/* ============== 横向柱状图(纯 View 实现)============== */
+
+interface BarItem {
+ label: string;
+ stacks: { value: number; color: string }[];
+}
+
+function DailyBarChart({ data, maxVal: rawMax }: { data: BarItem[]; maxVal: number }) {
+ const maxVal = rawMax || 1;
return (
-
-
-
-
+
+ {data.map((item, i) => (
+
+ {item.label}
+ {item.stacks
+ .filter((s) => s.value > 0)
+ .map((stack, j) => {
+ const pct = Math.min((stack.value / maxVal) * 100, 100);
+ return (
+
+ {/* 背景轨道 */}
+
+
+
+
+ ¥{formatMoney(stack.value)}
+
+
+ );
+ })}
-
-
-
- {item.categoryName}
-
- {getTypeLabel(item.type)}
-
-
- {item.count} 笔 · {percentText}
-
-
-
- ¥{formatMoney(item.amount)}
-
-
-
-
-
-
- );
-}
-
-function MemberRow({ item, isLast }: { item: MemberStat; isLast: boolean }) {
- const balance = item.income - item.expense;
-
- return (
-
-
- {item.nickname.slice(0, 1)}
-
-
-
- {item.nickname}
-
-
- {item.count} 笔 · 支出 ¥{formatMoney(item.expense)} · 收入 ¥{formatMoney(item.income)}
-
-
- = 0 ? "text-emerald-600" : "text-rose-500"}`}>
- {balance >= 0 ? "+" : "-"}¥{formatMoney(Math.abs(balance))}
-
+ ))}
);
}
diff --git a/code/app/app-rn/src/app/finance/(tabs)/home/index.tsx b/code/app/app-rn/src/app/finance/(tabs)/home/index.tsx
index 0040b0d5..de2f9c66 100644
--- a/code/app/app-rn/src/app/finance/(tabs)/home/index.tsx
+++ b/code/app/app-rn/src/app/finance/(tabs)/home/index.tsx
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
-import { BottomSheet, Button, Input, Label, Menu, TextField } from "heroui-native";
+import { Button, Label, Menu, TextField } from "heroui-native";
+import BottomSheet from "@/components/BottomSheet";
import { router, useFocusEffect } from "expo-router";
import {
CalendarDaysIcon,
@@ -47,10 +48,6 @@ function formatMoney(value: number) {
return moneyFormatter.format(value || 0);
}
-function getCategoryTypeLabel(type: FinanceType) {
- return type === FinanceType.Income ? "收入" : "支出";
-}
-
function createDefaultFilter(): FilterState {
return {
startDate: getStartOfMonth(),
@@ -152,32 +149,33 @@ export default function Home() {
}
const res = await loadDataBySqlApi({
- sql: `
- select
- f.id,
- f.family_id,
- f.category_id,
- f.amount,
- f.remark,
- f.record_time,
- f.create_by,
- f.create_time,
- f.update_by,
- f.update_time,
- c.name as category_name,
- c.type as category_type,
- c.icon_file_key as category_icon_file_key,
- coalesce(m.nickname, u.name) as member_nickname,
- coalesce(um.nickname, uu.name) as update_member_nickname
- from b_finance f
- left join b_finance_category c on c.id = f.category_id
- left join b_family_member m on m.family_id = f.family_id and m.user_id = f.create_by
- left join b_user u on u.id = f.create_by
- left join b_family_member um on um.family_id = f.family_id and um.user_id = f.update_by
- left join b_user uu on uu.id = f.update_by
- where ${conditions.join(" and ")}
- order by f.record_time desc, f.create_time desc, f.id desc
- `,
+ sql:
+ "select\n" +
+ " f.id,\n" +
+ " f.family_id,\n" +
+ " f.category_id,\n" +
+ " f.amount,\n" +
+ " f.remark,\n" +
+ " f.record_time,\n" +
+ " f.create_by,\n" +
+ " f.create_time,\n" +
+ " f.update_by,\n" +
+ " f.update_time,\n" +
+ " c.name as category_name,\n" +
+ " c.type as category_type,\n" +
+ " c.icon_file_key as category_icon_file_key,\n" +
+ " coalesce(m.nickname, u.name) as member_nickname,\n" +
+ " coalesce(um.nickname, uu.name) as update_member_nickname\n" +
+ "from b_finance f\n" +
+ "left join b_finance_category c on c.id = f.category_id\n" +
+ "left join b_family_member m on m.family_id = f.family_id and m.user_id = f.create_by\n" +
+ "left join b_user u on u.id = f.create_by\n" +
+ "left join b_family_member um on um.family_id = f.family_id and um.user_id = f.update_by\n" +
+ "left join b_user uu on uu.id = f.update_by\n" +
+ "where " +
+ conditions.join(" and ") +
+ "\n" +
+ "order by f.record_time desc, f.create_time desc, f.id desc",
args,
});
@@ -273,23 +271,31 @@ export default function Home() {
() => (
-
- 当前范围
-
+
+
{filter.startDate} 至 {filter.endDate}
-
-
-
- 筛选{activeFilterCount > 0 ? ` ${activeFilterCount}` : ""}
-
-
+
+
+
+
+
+
+
+ 筛选{activeFilterCount > 0 ? ` ${activeFilterCount}` : ""}
+
+
+
@@ -306,53 +312,28 @@ export default function Home() {
+
+ ),
+ [activeFilterCount, filter, handleRefresh, openFilterSheet, selectedCategory, totals],
+ );
-
-
-
- 分类:{selectedCategory ? selectedCategory.name : "全部"}
-
-
-
-
- 备注:{filter.keyword.trim() || "全部"}
-
+ const renderSectionHeader = useCallback(({ section }: { section: any }) => {
+ const billSection = section as FinanceBillSection;
+
+ return (
+
+
+
+ {dayjs(billSection.title).format("YYYY年MM月DD日")}
+
+
+ -¥{formatMoney(billSection.expenseTotal)}
+ +¥{formatMoney(billSection.incomeTotal)}
- ),
- [activeFilterCount, filter, openFilterSheet, selectedCategory, totals],
- );
-
- const renderSectionHeader = useCallback(
- ({ section }: { section: any }) => {
- const billSection = section as FinanceBillSection;
-
- return (
-
-
-
-
- {dayjs(billSection.title).format("MM月DD日")}
-
-
- {dayjs(billSection.title).format("YYYY年")}
-
-
-
-
- 支出 ¥{formatMoney(billSection.expenseTotal)}
-
-
- 收入 ¥{formatMoney(billSection.incomeTotal)}
-
-
-
-
- );
- },
- [],
- );
+ );
+ }, []);
const renderItem = useCallback(
({ item }: { item: FinanceBill }) => {
@@ -392,12 +373,11 @@ export default function Home() {
-
+
{isIncome ? "+" : "-"}¥{formatMoney(Number(item.amount))}
-
- {getCategoryTypeLabel(item.category_type)}
-
);
@@ -445,7 +425,10 @@ export default function Home() {
showsVerticalScrollIndicator={false}
refreshing={refreshing}
onRefresh={handleRefresh}
- contentContainerStyle={sections.length === 0 ? { flexGrow: 1 } : undefined}
+ contentContainerStyle={[
+ { paddingHorizontal: 16 },
+ sections.length === 0 ? { flexGrow: 1 } : undefined,
+ ]}
ListEmptyComponent={() => (
-
+
筛选记账
@@ -488,7 +471,7 @@ export default function Home() {
- setDraftFilter((prev) => ({ ...prev, keyword }))}
placeholder="按备注模糊搜索"
@@ -548,7 +531,12 @@ export default function Home() {
}}
/>
-
();
@@ -82,7 +79,7 @@ export default function Add() {
const selectedCategory = useMemo(
() => categories.find((category) => category.id === selectedCategoryId) ?? null,
- [categories, selectedCategoryId]
+ [categories, selectedCategoryId],
);
const isSheetOpen = selectedCategory != null;
@@ -164,28 +161,38 @@ export default function Add() {
};
if (isEdit) {
- await saveDataApi([{
- table_name: "b_finance",
- key_field: "id",
- inserts: [],
- updates: [row],
- deletes: [],
- }]);
+ await saveDataApi([
+ {
+ table_name: "b_finance",
+ key_field: "id",
+ inserts: [],
+ updates: [row],
+ deletes: [],
+ },
+ ]);
} else {
const idRes = await getUniqueIdApi(1);
const newId = idRes.data?.[0];
- if (!newId) { toast.error("获取ID失败"); stopLoading(); return; }
+ if (!newId) {
+ toast.error("获取ID失败");
+ stopLoading();
+ return;
+ }
- await saveDataApi([{
- table_name: "b_finance",
- key_field: "id",
- inserts: [{
- ...row,
- id: newId,
- }],
- updates: [],
- deletes: [],
- }]);
+ await saveDataApi([
+ {
+ table_name: "b_finance",
+ key_field: "id",
+ inserts: [
+ {
+ ...row,
+ id: newId,
+ },
+ ],
+ updates: [],
+ deletes: [],
+ },
+ ]);
}
const label = activeTab === "0" ? "支出" : "收入";
@@ -206,7 +213,21 @@ export default function Add() {
stopLoading();
}
},
- [amount, selectedCategory, activeTab, resetForm, isLoading, startLoading, stopLoading, familyId, date, remark, editId, isEdit, router]
+ [
+ amount,
+ selectedCategory,
+ activeTab,
+ resetForm,
+ isLoading,
+ startLoading,
+ stopLoading,
+ familyId,
+ date,
+ remark,
+ editId,
+ isEdit,
+ router,
+ ],
);
return (
@@ -230,13 +251,13 @@ export default function Add() {
{/* 搜索 */}
-
-
+
+
- {/* 记账 BottomSheet */}
+ {/* 璁拌处 BottomSheet */}
-
-
+
{/* 按钮 */}
- handleSave(false)}
- className="flex-1"
- >
+ handleSave(false)} className="flex-1">
保存
{!isEdit && (
- handleSave(true)}
- className="flex-1"
- variant="secondary"
- >
+ handleSave(true)} className="flex-1" variant="secondary">
保存并再记
)}
@@ -349,7 +368,10 @@ export default function Add() {
mode="date"
format="YYYY-MM-DD"
onClose={() => setShowDatePicker(false)}
- onConfirm={(val) => { setDate(val); setShowDatePicker(false); }}
+ onConfirm={(val) => {
+ setDate(val);
+ setShowDatePicker(false);
+ }}
/>
diff --git a/code/app/app-rn/src/app/finance/category/index.tsx b/code/app/app-rn/src/app/finance/category/index.tsx
index 1a3c9dc3..0844118e 100644
--- a/code/app/app-rn/src/app/finance/category/index.tsx
+++ b/code/app/app-rn/src/app/finance/category/index.tsx
@@ -2,7 +2,8 @@ import { useCallback, useMemo, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
import { Stack } from "expo-router";
import ImagePicker from "react-native-image-crop-picker";
-import { BottomSheet, Button, Input, Label, Tabs, TextField } from "heroui-native";
+import { Button, Label, Tabs, TextField } from "heroui-native";
+import BottomSheet from "@/components/BottomSheet";
import { GripVerticalIcon, ImageIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react-native";
import Sortable, { SortableGridRenderItem } from "react-native-sortables";
import PageLayout from "@/layouts/PageLayout";
@@ -66,7 +67,7 @@ export default function FinanceCategoryList() {
categories
.filter((item) => item.type === Number(activeType))
.sort((a, b) => a.sort_number - b.sort_number || a.id - b.id),
- [activeType, categories]
+ [activeType, categories],
);
const closeSheet = useCallback(() => {
@@ -124,7 +125,7 @@ export default function FinanceCategoryList() {
iconMime: file.type,
iconSource: FinanceCategoryIconSource.Custom,
}
- : prev
+ : prev,
);
toast.success("图标已上传", { id: toastId });
} catch (error) {
@@ -160,7 +161,7 @@ export default function FinanceCategoryList() {
toast.error(res.message || "删除失败");
}
},
- [familyId, loadCategories, removeCategory]
+ [familyId, loadCategories, removeCategory],
);
const handleDragEnd = useCallback(
@@ -186,7 +187,7 @@ export default function FinanceCategoryList() {
toast.error(res.message || "排序保存失败");
}
},
- [familyId, loadCategories]
+ [familyId, loadCategories],
);
const handleSave = useCallback(async () => {
@@ -284,7 +285,7 @@ export default function FinanceCategoryList() {
),
- [handleDelete, handleEdit]
+ [handleDelete, handleEdit],
);
return (
@@ -326,10 +327,15 @@ export default function FinanceCategoryList() {
- { if (!open) closeSheet(); }}>
+ {
+ if (!open) closeSheet();
+ }}
+ >
-
+
{form && (
@@ -349,7 +355,7 @@ export default function FinanceCategoryList() {
- setForm((prev) => (prev ? { ...prev, name } : prev))}
placeholder="例如:咖啡"
diff --git a/code/app/app-rn/src/app/finance/detail/[id].tsx b/code/app/app-rn/src/app/finance/detail/[id].tsx
index d62f1bd2..d5c0a636 100644
--- a/code/app/app-rn/src/app/finance/detail/[id].tsx
+++ b/code/app/app-rn/src/app/finance/detail/[id].tsx
@@ -47,31 +47,30 @@ export default function FinanceDetail() {
if (!id || !familyId) return;
const res = await loadDataBySqlApi({
- sql: `
- select
- f.id,
- f.family_id,
- f.category_id,
- f.amount,
- f.remark,
- f.record_time,
- f.create_by,
- f.create_time,
- f.update_by,
- f.update_time,
- c.name as category_name,
- c.type as category_type,
- c.icon_file_key as category_icon_file_key,
- coalesce(m.nickname, u.name) as member_nickname,
- coalesce(um.nickname, uu.name) as update_member_nickname
- from b_finance f
- left join b_finance_category c on c.id = f.category_id
- left join b_family_member m on m.family_id = f.family_id and m.user_id = f.create_by
- left join b_user u on u.id = f.create_by
- left join b_family_member um on um.family_id = f.family_id and um.user_id = f.update_by
- left join b_user uu on uu.id = f.update_by
- where f.id = ? and f.family_id = ?
- `,
+ sql:
+ "select\n" +
+ " f.id,\n" +
+ " f.family_id,\n" +
+ " f.category_id,\n" +
+ " f.amount,\n" +
+ " f.remark,\n" +
+ " f.record_time,\n" +
+ " f.create_by,\n" +
+ " f.create_time,\n" +
+ " f.update_by,\n" +
+ " f.update_time,\n" +
+ " c.name as category_name,\n" +
+ " c.type as category_type,\n" +
+ " c.icon_file_key as category_icon_file_key,\n" +
+ " coalesce(m.nickname, u.name) as member_nickname,\n" +
+ " coalesce(um.nickname, uu.name) as update_member_nickname\n" +
+ "from b_finance f\n" +
+ "left join b_finance_category c on c.id = f.category_id\n" +
+ "left join b_family_member m on m.family_id = f.family_id and m.user_id = f.create_by\n" +
+ "left join b_user u on u.id = f.create_by\n" +
+ "left join b_family_member um on um.family_id = f.family_id and um.user_id = f.update_by\n" +
+ "left join b_user uu on uu.id = f.update_by\n" +
+ "where f.id = ? and f.family_id = ?",
args: [Number(id), familyId],
});
@@ -116,7 +115,9 @@ export default function FinanceDetail() {
{bill.category_name || "未分类"}
-
+
{isIncome ? "+" : "-"}¥{formatMoney(bill.amount)}
diff --git a/code/app/app-rn/src/components/BottomSheet.tsx b/code/app/app-rn/src/components/BottomSheet.tsx
new file mode 100644
index 00000000..649f9c61
--- /dev/null
+++ b/code/app/app-rn/src/components/BottomSheet.tsx
@@ -0,0 +1,182 @@
+import {
+ BottomSheetBackdrop,
+ BottomSheetModal,
+ BottomSheetView,
+ useBottomSheetTimingConfigs,
+} from "@gorhom/bottom-sheet";
+import type { BottomSheetModalProps } from "@gorhom/bottom-sheet";
+import { ComponentProps, forwardRef, ReactNode, useCallback, useEffect, useRef } from "react";
+import { Text, type TextInput, type TextProps } from "react-native";
+import { Easing } from "react-native-reanimated";
+import { Input, useBottomSheetAwareHandlers } from "heroui-native";
+
+type AppBottomSheetProps = Omit<
+ BottomSheetModalProps,
+ "backdropComponent" | "animationConfigs" | "onDismiss"
+> & {
+ children: ReactNode;
+ isOpen: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ backdropOpacity?: number;
+ animationDuration?: number;
+};
+
+type AppBottomSheetContentProps = ComponentProps & {
+ animateOnMount?: boolean;
+ animation?: "disabled" | "disable-all" | boolean;
+ backgroundClassName?: string;
+ contentContainerClassName?: string;
+ enableBlurKeyboardOnGesture?: boolean;
+};
+
+function AppBottomSheetRoot({
+ children,
+ isOpen,
+ onOpenChange,
+ backdropOpacity = 0.3,
+ animationDuration = 300,
+ enablePanDownToClose = true,
+ // interactive: 键盘弹出时 sheet 上移到键盘上方(gorhom 默认值)
+ // extend 只是拉到最高 snap point,不上移 → input 仍被键盘遮挡
+ keyboardBehavior = "interactive",
+ keyboardBlurBehavior = "restore",
+ // adjustPan: 让 gorhom 自己处理键盘避让(gorhom 默认值)
+ // adjustResize 会在 Android 上导致 interactive 模式被跳过(见 BottomSheet.js 源码)
+ android_keyboardInputMode = "adjustPan",
+ enableBlurKeyboardOnGesture = true,
+ backgroundStyle,
+ handleIndicatorStyle,
+ ...props
+}: AppBottomSheetProps) {
+ const bottomSheetModalRef = useRef(null);
+ const wasPresentedRef = useRef(false);
+
+ const animationConfigs = useBottomSheetTimingConfigs({
+ duration: animationDuration,
+ easing: Easing.out(Easing.cubic),
+ });
+
+ const renderBackdrop = useCallback(
+ (backdropProps: ComponentProps) => (
+
+ ),
+ [backdropOpacity],
+ );
+
+ useEffect(() => {
+ if (isOpen) {
+ wasPresentedRef.current = true;
+ bottomSheetModalRef.current?.present();
+ return;
+ }
+
+ if (wasPresentedRef.current) {
+ bottomSheetModalRef.current?.dismiss();
+ }
+ }, [isOpen]);
+
+ const handleDismiss = useCallback(() => {
+ wasPresentedRef.current = false;
+ onOpenChange?.(false);
+ }, [onOpenChange]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+function AppBottomSheetPortal({ children }: { children: ReactNode }) {
+ return <>{children}>;
+}
+
+function AppBottomSheetOverlay() {
+ return null;
+}
+
+function AppBottomSheetContent({
+ children,
+ className,
+ contentContainerClassName,
+ animateOnMount: _animateOnMount,
+ animation: _animation,
+ backgroundClassName: _backgroundClassName,
+ style,
+ ...props
+}: AppBottomSheetContentProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+function AppBottomSheetTitle(props: TextProps) {
+ return ;
+}
+
+function AppBottomSheetDescription(props: TextProps) {
+ return ;
+}
+
+const AppBottomSheetInput = forwardRef>(
+ ({ onFocus, onBlur, ...props }, ref) => {
+ // 用 heroui 原生 Input + useBottomSheetAwareHandlers 接入 sheet 键盘状态。
+ // hook 内部调用 gorhom 的 useBottomSheetInternal(true)(unsafe 模式),
+ // 在 sheet context 外返回 no-op handlers,在 sheet 内则把 input 的 focus/blur
+ // 注册到 animatedKeyboardState,配合 keyboardBehavior="interactive" 触发上移避让。
+ // 这样无需用 BottomSheetTextInput,保留 heroui Input 的全部样式与 API。
+ const handlers = useBottomSheetAwareHandlers();
+ return (
+ {
+ handlers.onFocus(e);
+ onFocus?.(e);
+ }}
+ onBlur={(e) => {
+ handlers.onBlur(e);
+ onBlur?.(e);
+ }}
+ {...props}
+ />
+ );
+ },
+);
+
+AppBottomSheetInput.displayName = "AppBottomSheetInput";
+
+const AppBottomSheet = Object.assign(AppBottomSheetRoot, {
+ Portal: AppBottomSheetPortal,
+ Overlay: AppBottomSheetOverlay,
+ Content: AppBottomSheetContent,
+ Title: AppBottomSheetTitle,
+ Description: AppBottomSheetDescription,
+ Input: AppBottomSheetInput,
+});
+
+export default AppBottomSheet;
diff --git a/code/app/app-rn/src/components/DateTime.tsx b/code/app/app-rn/src/components/DateTime.tsx
index 59f24a7b..ce4a96b5 100644
--- a/code/app/app-rn/src/components/DateTime.tsx
+++ b/code/app/app-rn/src/components/DateTime.tsx
@@ -1,6 +1,7 @@
import DateTimePicker from "react-native-dates-picker";
import dayjs from "dayjs";
-import { BottomSheet, Button, Spinner, useThemeColor } from "heroui-native";
+import { Button, Spinner, useThemeColor } from "heroui-native";
+import BottomSheet from "@/components/BottomSheet";
import { useState, useEffect, useCallback, useRef } from "react";
import { View } from "react-native";
import { CheckIcon } from "lucide-react-native";
@@ -8,12 +9,7 @@ import { FadeIn } from "react-native-reanimated";
export type DatePickerMode = "year" | "month" | "date" | "time" | "datetime";
-export type DateFormatType =
- | "YYYY"
- | "YYYY-MM"
- | "YYYY-MM-DD"
- | "HH:mm:ss"
- | "YYYY-MM-DD HH:mm";
+export type DateFormatType = "YYYY" | "YYYY-MM" | "YYYY-MM-DD" | "HH:mm:ss" | "YYYY-MM-DD HH:mm";
interface DatePickerProps {
value: Date | string;
@@ -68,7 +64,7 @@ export default function DateTime({
}, [show, value]);
// 关闭:BottomSheet 的 onOpenChange(false) 或 confirm 后 setOpen(false)
- // 立刻通知父组件,不延迟 — BottomSheet 自身驱动关闭动画
+ // 立刻通知父组件,不延迟 —— BottomSheet 自身驱动关闭动画
useEffect(() => {
if (!open && show) {
onCloseRef.current?.();
@@ -94,8 +90,8 @@ export default function DateTime({
-
-
+
+
-
-
+
+
= {
function LoadingVisual() {
const rotateValue = useRef(new Animated.Value(0)).current;
const pulseValue = useRef(new Animated.Value(0)).current;
+ const accentColor = useThemeColor("accent");
useEffect(() => {
const rotateAnimation = Animated.loop(
@@ -85,15 +87,15 @@ function LoadingVisual() {
return (
-
+
-
+
diff --git a/code/app/app-rn/src/components/LoadingOverlay.tsx b/code/app/app-rn/src/components/LoadingOverlay.tsx
index 81da2755..3a6d1d0b 100644
--- a/code/app/app-rn/src/components/LoadingOverlay.tsx
+++ b/code/app/app-rn/src/components/LoadingOverlay.tsx
@@ -8,9 +8,6 @@ export function LoadingOverlay({ visible }: LoadingOverlayProps) {
if (!visible) return null;
return (
-
+
);
}
diff --git a/code/app/app-rn/src/components/iconfont/IconAppClothes.tsx b/code/app/app-rn/src/components/iconfont/IconAppClothes.tsx
index c7c30665..80ae3678 100644
--- a/code/app/app-rn/src/components/iconfont/IconAppClothes.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconAppClothes.tsx
@@ -1,72 +1,66 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconAppClothes: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconAppClothes: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconAppClothes);
\ No newline at end of file
+export default React.memo(IconAppClothes);
diff --git a/code/app/app-rn/src/components/iconfont/IconAppFinance.tsx b/code/app/app-rn/src/components/iconfont/IconAppFinance.tsx
index 82e64fae..34056363 100644
--- a/code/app/app-rn/src/components/iconfont/IconAppFinance.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconAppFinance.tsx
@@ -1,56 +1,50 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconAppFinance: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconAppFinance: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconAppFinance);
\ No newline at end of file
+export default React.memo(IconAppFinance);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceCanyin.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceCanyin.tsx
index d76bac24..0b2ddfb7 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceCanyin.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceCanyin.tsx
@@ -1,108 +1,102 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceCanyin: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceCanyin: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceCanyin);
\ No newline at end of file
+export default React.memo(IconFinanceCanyin);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceChongwu.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceChongwu.tsx
index 275bdba9..0760db50 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceChongwu.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceChongwu.tsx
@@ -1,88 +1,82 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceChongwu: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceChongwu: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceChongwu);
\ No newline at end of file
+export default React.memo(IconFinanceChongwu);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceGongzi.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceGongzi.tsx
index bd7c5128..d740fc71 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceGongzi.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceGongzi.tsx
@@ -1,60 +1,54 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceGongzi: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceGongzi: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceGongzi);
\ No newline at end of file
+export default React.memo(IconFinanceGongzi);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceGouwu.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceGouwu.tsx
index efea2fa6..b5b70994 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceGouwu.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceGouwu.tsx
@@ -1,80 +1,74 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceGouwu: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceGouwu: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceGouwu);
\ No newline at end of file
+export default React.memo(IconFinanceGouwu);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceJianzhi.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceJianzhi.tsx
index 410de4e6..e4416469 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceJianzhi.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceJianzhi.tsx
@@ -1,60 +1,54 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceJianzhi: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceJianzhi: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceJianzhi);
\ No newline at end of file
+export default React.memo(IconFinanceJianzhi);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceJiaotong.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceJiaotong.tsx
index 256aee2e..1478ab55 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceJiaotong.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceJiaotong.tsx
@@ -1,88 +1,82 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceJiaotong: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceJiaotong: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceJiaotong);
\ No newline at end of file
+export default React.memo(IconFinanceJiaotong);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceLijin.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceLijin.tsx
index a3f376cf..20ce2592 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceLijin.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceLijin.tsx
@@ -1,44 +1,38 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceLijin: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceLijin: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceLijin);
\ No newline at end of file
+export default React.memo(IconFinanceLijin);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceLingshi.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceLingshi.tsx
index 10b9834b..bfbb3cc2 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceLingshi.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceLingshi.tsx
@@ -1,88 +1,82 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceLingshi: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceLingshi: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceLingshi);
\ No newline at end of file
+export default React.memo(IconFinanceLingshi);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceShicai.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceShicai.tsx
index 79fb1138..f82d873b 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceShicai.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceShicai.tsx
@@ -1,92 +1,86 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceShicai: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceShicai: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceShicai);
\ No newline at end of file
+export default React.memo(IconFinanceShicai);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceShouru.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceShouru.tsx
index 1babda16..4c7245b5 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceShouru.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceShouru.tsx
@@ -1,44 +1,38 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceShouru: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceShouru: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceShouru);
\ No newline at end of file
+export default React.memo(IconFinanceShouru);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceShuidian.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceShuidian.tsx
index 88bb746b..ba2bbfe3 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceShuidian.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceShuidian.tsx
@@ -1,44 +1,38 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceShuidian: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceShuidian: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceShuidian);
\ No newline at end of file
+export default React.memo(IconFinanceShuidian);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceShuma.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceShuma.tsx
index 3c7fdd70..1a88b2e6 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceShuma.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceShuma.tsx
@@ -1,72 +1,66 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceShuma: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceShuma: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceShuma);
\ No newline at end of file
+export default React.memo(IconFinanceShuma);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceYifu.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceYifu.tsx
index ba6dc7f6..43b988b6 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceYifu.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceYifu.tsx
@@ -1,76 +1,70 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceYifu: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceYifu: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceYifu);
\ No newline at end of file
+export default React.memo(IconFinanceYifu);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceYiliao.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceYiliao.tsx
index 354e55f0..bcabf2db 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceYiliao.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceYiliao.tsx
@@ -1,96 +1,90 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceYiliao: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceYiliao: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceYiliao);
\ No newline at end of file
+export default React.memo(IconFinanceYiliao);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceYouxi.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceYouxi.tsx
index c2fd1dfe..1c12e2e0 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceYouxi.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceYouxi.tsx
@@ -1,564 +1,558 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceYouxi: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceYouxi: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceYouxi);
\ No newline at end of file
+export default React.memo(IconFinanceYouxi);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceYule.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceYule.tsx
index 8a5d3c79..4a4d7962 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceYule.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceYule.tsx
@@ -1,68 +1,62 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceYule: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceYule: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceYule);
\ No newline at end of file
+export default React.memo(IconFinanceYule);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceYundong.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceYundong.tsx
index 3ffc0b86..c39870e4 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceYundong.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceYundong.tsx
@@ -1,52 +1,46 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceYundong: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceYundong: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceYundong);
\ No newline at end of file
+export default React.memo(IconFinanceYundong);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceZhichu.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceZhichu.tsx
index 804208fa..7f99f6a7 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceZhichu.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceZhichu.tsx
@@ -1,44 +1,38 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceZhichu: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceZhichu: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceZhichu);
\ No newline at end of file
+export default React.memo(IconFinanceZhichu);
diff --git a/code/app/app-rn/src/components/iconfont/IconFinanceZhufang.tsx b/code/app/app-rn/src/components/iconfont/IconFinanceZhufang.tsx
index 9f3658e5..06000de8 100644
--- a/code/app/app-rn/src/components/iconfont/IconFinanceZhufang.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconFinanceZhufang.tsx
@@ -1,48 +1,39 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconFinanceZhufang: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconFinanceZhufang: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconFinanceZhufang);
\ No newline at end of file
+export default React.memo(IconFinanceZhufang);
diff --git a/code/app/app-rn/src/components/iconfont/IconQq.tsx b/code/app/app-rn/src/components/iconfont/IconQq.tsx
index 49bb0a15..5e75386c 100644
--- a/code/app/app-rn/src/components/iconfont/IconQq.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconQq.tsx
@@ -1,68 +1,62 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconQq: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconQq: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconQq);
\ No newline at end of file
+export default React.memo(IconQq);
diff --git a/code/app/app-rn/src/components/iconfont/IconWeixin.tsx b/code/app/app-rn/src/components/iconfont/IconWeixin.tsx
index 5eaf4d38..2d64d235 100644
--- a/code/app/app-rn/src/components/iconfont/IconWeixin.tsx
+++ b/code/app/app-rn/src/components/iconfont/IconWeixin.tsx
@@ -1,36 +1,30 @@
/* eslint-disable */
/* tslint:disable */
-import React from 'react';
-import type { FunctionComponent } from 'react';
-import type { ViewProps } from 'react-native';
-import { Svg, GProps, Path } from 'react-native-svg';
-import { getIconColor } from './helper';
+import React from "react";
+import type { FunctionComponent } from "react";
+import type { ViewProps } from "react-native";
+import { Svg, GProps, Path } from "react-native-svg";
+import { getIconColor } from "./helper";
-interface Props extends Omit, ViewProps {
+interface Props extends Omit, ViewProps {
size?: number;
color?: string | string[];
}
-const IconWeixin: FunctionComponent = ({
- size = 18,
- color,
- ...rest
-}) => {
+const IconWeixin: FunctionComponent = ({ size = 18, color, ...rest }) => {
return (
-
-
);
};
-export default React.memo(IconWeixin);
\ No newline at end of file
+export default React.memo(IconWeixin);
diff --git a/code/app/app-rn/src/components/iconfont/helper.ts b/code/app/app-rn/src/components/iconfont/helper.ts
index 60d6d713..7c8361f5 100644
--- a/code/app/app-rn/src/components/iconfont/helper.ts
+++ b/code/app/app-rn/src/components/iconfont/helper.ts
@@ -1,12 +1,10 @@
/* tslint:disable */
/* eslint-disable */
-export const getIconColor = (color: string | string[] | undefined, index: number, defaultColor: string) => {
- return color
- ? (
- typeof color === 'string'
- ? color
- : color[index] || defaultColor
- )
- : defaultColor;
+export const getIconColor = (
+ color: string | string[] | undefined,
+ index: number,
+ defaultColor: string,
+) => {
+ return color ? (typeof color === "string" ? color : color[index] || defaultColor) : defaultColor;
};
diff --git a/code/app/app-rn/src/components/iconfont/index.tsx b/code/app/app-rn/src/components/iconfont/index.tsx
index f990ae07..a5dcad19 100644
--- a/code/app/app-rn/src/components/iconfont/index.tsx
+++ b/code/app/app-rn/src/components/iconfont/index.tsx
@@ -1,57 +1,80 @@
/* tslint:disable */
/* eslint-disable */
-import React, { FunctionComponent } from 'react';
-import { ViewProps } from 'react-native';
-import { GProps } from 'react-native-svg';
-import IconFinanceYifu from './IconFinanceYifu';
-import IconFinanceJianzhi from './IconFinanceJianzhi';
-import IconFinanceLijin from './IconFinanceLijin';
-import IconFinanceGongzi from './IconFinanceGongzi';
-import IconFinanceChongwu from './IconFinanceChongwu';
-import IconFinanceShouru from './IconFinanceShouru';
-import IconFinanceZhufang from './IconFinanceZhufang';
-import IconFinanceShuidian from './IconFinanceShuidian';
-import IconFinanceZhichu from './IconFinanceZhichu';
-import IconFinanceYule from './IconFinanceYule';
-import IconFinanceYundong from './IconFinanceYundong';
-import IconFinanceYouxi from './IconFinanceYouxi';
-import IconFinanceGouwu from './IconFinanceGouwu';
-import IconFinanceLingshi from './IconFinanceLingshi';
-import IconFinanceYiliao from './IconFinanceYiliao';
-import IconFinanceShuma from './IconFinanceShuma';
-import IconFinanceCanyin from './IconFinanceCanyin';
-import IconFinanceJiaotong from './IconFinanceJiaotong';
-import IconFinanceShicai from './IconFinanceShicai';
-import IconAppFinance from './IconAppFinance';
-import IconAppClothes from './IconAppClothes';
-import IconWeixin from './IconWeixin';
-import IconQq from './IconQq';
-export { default as IconFinanceYifu } from './IconFinanceYifu';
-export { default as IconFinanceJianzhi } from './IconFinanceJianzhi';
-export { default as IconFinanceLijin } from './IconFinanceLijin';
-export { default as IconFinanceGongzi } from './IconFinanceGongzi';
-export { default as IconFinanceChongwu } from './IconFinanceChongwu';
-export { default as IconFinanceShouru } from './IconFinanceShouru';
-export { default as IconFinanceZhufang } from './IconFinanceZhufang';
-export { default as IconFinanceShuidian } from './IconFinanceShuidian';
-export { default as IconFinanceZhichu } from './IconFinanceZhichu';
-export { default as IconFinanceYule } from './IconFinanceYule';
-export { default as IconFinanceYundong } from './IconFinanceYundong';
-export { default as IconFinanceYouxi } from './IconFinanceYouxi';
-export { default as IconFinanceGouwu } from './IconFinanceGouwu';
-export { default as IconFinanceLingshi } from './IconFinanceLingshi';
-export { default as IconFinanceYiliao } from './IconFinanceYiliao';
-export { default as IconFinanceShuma } from './IconFinanceShuma';
-export { default as IconFinanceCanyin } from './IconFinanceCanyin';
-export { default as IconFinanceJiaotong } from './IconFinanceJiaotong';
-export { default as IconFinanceShicai } from './IconFinanceShicai';
-export { default as IconAppFinance } from './IconAppFinance';
-export { default as IconAppClothes } from './IconAppClothes';
-export { default as IconWeixin } from './IconWeixin';
-export { default as IconQq } from './IconQq';
+import React, { FunctionComponent } from "react";
+import { ViewProps } from "react-native";
+import { GProps } from "react-native-svg";
+import IconFinanceYifu from "./IconFinanceYifu";
+import IconFinanceJianzhi from "./IconFinanceJianzhi";
+import IconFinanceLijin from "./IconFinanceLijin";
+import IconFinanceGongzi from "./IconFinanceGongzi";
+import IconFinanceChongwu from "./IconFinanceChongwu";
+import IconFinanceShouru from "./IconFinanceShouru";
+import IconFinanceZhufang from "./IconFinanceZhufang";
+import IconFinanceShuidian from "./IconFinanceShuidian";
+import IconFinanceZhichu from "./IconFinanceZhichu";
+import IconFinanceYule from "./IconFinanceYule";
+import IconFinanceYundong from "./IconFinanceYundong";
+import IconFinanceYouxi from "./IconFinanceYouxi";
+import IconFinanceGouwu from "./IconFinanceGouwu";
+import IconFinanceLingshi from "./IconFinanceLingshi";
+import IconFinanceYiliao from "./IconFinanceYiliao";
+import IconFinanceShuma from "./IconFinanceShuma";
+import IconFinanceCanyin from "./IconFinanceCanyin";
+import IconFinanceJiaotong from "./IconFinanceJiaotong";
+import IconFinanceShicai from "./IconFinanceShicai";
+import IconAppFinance from "./IconAppFinance";
+import IconAppClothes from "./IconAppClothes";
+import IconWeixin from "./IconWeixin";
+import IconQq from "./IconQq";
+export { default as IconFinanceYifu } from "./IconFinanceYifu";
+export { default as IconFinanceJianzhi } from "./IconFinanceJianzhi";
+export { default as IconFinanceLijin } from "./IconFinanceLijin";
+export { default as IconFinanceGongzi } from "./IconFinanceGongzi";
+export { default as IconFinanceChongwu } from "./IconFinanceChongwu";
+export { default as IconFinanceShouru } from "./IconFinanceShouru";
+export { default as IconFinanceZhufang } from "./IconFinanceZhufang";
+export { default as IconFinanceShuidian } from "./IconFinanceShuidian";
+export { default as IconFinanceZhichu } from "./IconFinanceZhichu";
+export { default as IconFinanceYule } from "./IconFinanceYule";
+export { default as IconFinanceYundong } from "./IconFinanceYundong";
+export { default as IconFinanceYouxi } from "./IconFinanceYouxi";
+export { default as IconFinanceGouwu } from "./IconFinanceGouwu";
+export { default as IconFinanceLingshi } from "./IconFinanceLingshi";
+export { default as IconFinanceYiliao } from "./IconFinanceYiliao";
+export { default as IconFinanceShuma } from "./IconFinanceShuma";
+export { default as IconFinanceCanyin } from "./IconFinanceCanyin";
+export { default as IconFinanceJiaotong } from "./IconFinanceJiaotong";
+export { default as IconFinanceShicai } from "./IconFinanceShicai";
+export { default as IconAppFinance } from "./IconAppFinance";
+export { default as IconAppClothes } from "./IconAppClothes";
+export { default as IconWeixin } from "./IconWeixin";
+export { default as IconQq } from "./IconQq";
-export type IconNames = 'finance_yifu' | 'finance_jianzhi' | 'finance_lijin' | 'finance_gongzi' | 'finance_chongwu' | 'finance_shouru' | 'finance_zhufang' | 'finance_shuidian' | 'finance_zhichu' | 'finance_yule' | 'finance_yundong' | 'finance_youxi' | 'finance_gouwu' | 'finance_lingshi' | 'finance_yiliao' | 'finance_shuma' | 'finance_canyin' | 'finance_jiaotong' | 'finance_shicai' | 'app_finance' | 'app_clothes' | 'weixin' | 'QQ';
+export type IconNames =
+ | "finance_yifu"
+ | "finance_jianzhi"
+ | "finance_lijin"
+ | "finance_gongzi"
+ | "finance_chongwu"
+ | "finance_shouru"
+ | "finance_zhufang"
+ | "finance_shuidian"
+ | "finance_zhichu"
+ | "finance_yule"
+ | "finance_yundong"
+ | "finance_youxi"
+ | "finance_gouwu"
+ | "finance_lingshi"
+ | "finance_yiliao"
+ | "finance_shuma"
+ | "finance_canyin"
+ | "finance_jiaotong"
+ | "finance_shicai"
+ | "app_finance"
+ | "app_clothes"
+ | "weixin"
+ | "QQ";
interface Props extends GProps, ViewProps {
name: IconNames;
@@ -61,51 +84,51 @@ interface Props extends GProps, ViewProps {
let IconFont: FunctionComponent = ({ name, ...rest }) => {
switch (name) {
- case 'finance_yifu':
+ case "finance_yifu":
return ;
- case 'finance_jianzhi':
+ case "finance_jianzhi":
return ;
- case 'finance_lijin':
+ case "finance_lijin":
return ;
- case 'finance_gongzi':
+ case "finance_gongzi":
return ;
- case 'finance_chongwu':
+ case "finance_chongwu":
return ;
- case 'finance_shouru':
+ case "finance_shouru":
return ;
- case 'finance_zhufang':
+ case "finance_zhufang":
return ;
- case 'finance_shuidian':
+ case "finance_shuidian":
return ;
- case 'finance_zhichu':
+ case "finance_zhichu":
return ;
- case 'finance_yule':
+ case "finance_yule":
return ;
- case 'finance_yundong':
+ case "finance_yundong":
return ;
- case 'finance_youxi':
+ case "finance_youxi":
return ;
- case 'finance_gouwu':
+ case "finance_gouwu":
return ;
- case 'finance_lingshi':
+ case "finance_lingshi":
return ;
- case 'finance_yiliao':
+ case "finance_yiliao":
return ;
- case 'finance_shuma':
+ case "finance_shuma":
return ;
- case 'finance_canyin':
+ case "finance_canyin":
return ;
- case 'finance_jiaotong':
+ case "finance_jiaotong":
return ;
- case 'finance_shicai':
+ case "finance_shicai":
return ;
- case 'app_finance':
+ case "app_finance":
return ;
- case 'app_clothes':
+ case "app_clothes":
return ;
- case 'weixin':
+ case "weixin":
return ;
- case 'QQ':
+ case "QQ":
return ;
}
diff --git a/code/app/app-rn/src/components/layout/tabbar.tsx b/code/app/app-rn/src/components/layout/tabbar.tsx
index e6b69d3d..b34c5936 100644
--- a/code/app/app-rn/src/components/layout/tabbar.tsx
+++ b/code/app/app-rn/src/components/layout/tabbar.tsx
@@ -1,11 +1,7 @@
import { View, Text, StyleProp, ViewStyle, Pressable, StyleSheet } from "react-native";
import { LucideIcon } from "lucide-react-native";
import { cn } from "@/utils/cn";
-import Animated, {
- useSharedValue,
- useAnimatedStyle,
- withTiming,
-} from "react-native-reanimated";
+import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
import { useEffect } from "react";
import { useThemeColor } from "heroui-native";
diff --git a/code/app/app-rn/src/components/layout/view.tsx b/code/app/app-rn/src/components/layout/view.tsx
index d7b92ff8..0cc8d744 100644
--- a/code/app/app-rn/src/components/layout/view.tsx
+++ b/code/app/app-rn/src/components/layout/view.tsx
@@ -1,12 +1,5 @@
import React, { forwardRef, type ReactNode } from "react";
-import {
- SectionList,
- FlatList,
- View,
- ViewProps,
- StyleProp,
- ViewStyle,
-} from "react-native";
+import { SectionList, FlatList, View, ViewProps, StyleProp, ViewStyle } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
import { ScrollShadow } from "heroui-native";
import { LinearGradient } from "expo-linear-gradient";
@@ -92,7 +85,8 @@ LayoutView.SectionView = forwardRef(
props: React.ComponentProps> & LayoutConfig & PinnedListConfig,
ref: React.Ref>,
) => {
- const { header, footer, topExtraPadding, bottomExtraPadding, contentContainerStyle, ...rest } = props;
+ const { header, footer, topExtraPadding, bottomExtraPadding, contentContainerStyle, ...rest } =
+ props;
const padding = useContentPadding({ topExtraPadding, bottomExtraPadding });
if (header || footer) {
@@ -130,7 +124,11 @@ LayoutView.SectionView = forwardRef(
return (
-
+
);
@@ -251,7 +249,11 @@ LayoutView.LegendList = forwardRef(
return (
-
+
);
diff --git a/code/app/app-rn/src/global.css b/code/app/app-rn/src/global.css
index 5ade4518..392822e6 100644
--- a/code/app/app-rn/src/global.css
+++ b/code/app/app-rn/src/global.css
@@ -14,4 +14,4 @@
--accent-foreground: oklch(0.98 0 0);
}
}
-}
\ No newline at end of file
+}
diff --git a/code/app/app-rn/src/hooks/useChatWebSocket.tsx b/code/app/app-rn/src/hooks/useChatWebSocket.tsx
index 41a800a0..deae7c86 100644
--- a/code/app/app-rn/src/hooks/useChatWebSocket.tsx
+++ b/code/app/app-rn/src/hooks/useChatWebSocket.tsx
@@ -1,20 +1,8 @@
-import {
- createContext,
- ReactNode,
- useCallback,
- useContext,
- useEffect,
- useRef,
-} from "react";
+import { createContext, ReactNode, useCallback, useContext, useEffect, useRef } from "react";
import { toast } from "sonner-native";
import { useUserStore, useFamilyStore } from "@/store";
import { useChatStore } from "@/store/chat";
-import {
- ChatMessage,
- OnlineUser,
- SendChatPayload,
- normalizeChatMessage,
-} from "@/types/chat";
+import { ChatMessage, OnlineUser, SendChatPayload, normalizeChatMessage } from "@/types/chat";
const RECONNECT_BASE = 1000;
const RECONNECT_MAX = 30000;
@@ -212,11 +200,7 @@ function useChatWebSocketConnection(): ChatWebSocketValue {
export function ChatWebSocketProvider({ children }: { children: ReactNode }) {
const value = useChatWebSocketConnection();
- return (
-
- {children}
-
- );
+ return {children};
}
export function useChatWebSocket() {
diff --git a/code/app/app-rn/src/hooks/useLoading.ts b/code/app/app-rn/src/hooks/useLoading.ts
index 879e5580..7059e74d 100644
--- a/code/app/app-rn/src/hooks/useLoading.ts
+++ b/code/app/app-rn/src/hooks/useLoading.ts
@@ -1,4 +1,4 @@
-import { useState, useCallback, useRef } from "react";
+import { useState, useCallback, useRef, useEffect } from "react";
import { toast } from "sonner-native";
interface StopOptions {
@@ -16,17 +16,39 @@ interface RunLoadingOptions {
export function useLoading() {
const [isLoading, setIsLoading] = useState(false);
const toastIdRef = useRef(null);
+ // 用 ref 跟踪当前的 toast ID,避免闭包过期
+ const loadingRef = useRef(false);
+
+ // 组件卸载时确保 toast 也被关闭
+ useEffect(() => {
+ return () => {
+ const id = toastIdRef.current;
+ if (id) {
+ toast.dismiss(id);
+ toastIdRef.current = null;
+ }
+ };
+ }, []);
const startLoading = useCallback((message: string = "加载中...") => {
+ // 如果已有 loading 进行中,先关闭旧的 toast
+ if (loadingRef.current && toastIdRef.current) {
+ toast.dismiss(toastIdRef.current);
+ }
+ loadingRef.current = true;
setIsLoading(true);
- const id = toast.loading(message);
+ const id = toast.loading(message, { duration: Infinity });
toastIdRef.current = id;
return id;
}, []);
const stopLoading = useCallback((options?: StopOptions) => {
- setIsLoading(false);
const id = toastIdRef.current;
+ // 先关闭遮罩
+ loadingRef.current = false;
+ setIsLoading(false);
+ toastIdRef.current = null;
+
if (id) {
if (options?.type === "success") {
toast.success(options.message || "操作成功", { id });
@@ -35,16 +57,15 @@ export function useLoading() {
} else {
toast.dismiss(id);
}
- toastIdRef.current = null;
}
}, []);
const withLoading = useCallback(
- async (
+ async (
promise: Promise,
loadingMessage: string = "加载中...",
successMessage?: string,
- errorMessage?: string
+ errorMessage?: string,
): Promise => {
startLoading(loadingMessage);
try {
@@ -56,12 +77,12 @@ export function useLoading() {
throw error;
}
},
- [startLoading, stopLoading]
+ [startLoading, stopLoading],
);
const runWithLoading = useCallback(
- async (task: () => Promise, options?: RunLoadingOptions): Promise => {
- if (isLoading) return undefined;
+ async (task: () => Promise, options?: RunLoadingOptions): Promise => {
+ if (loadingRef.current) return undefined;
startLoading(options?.loadingMessage || "加载中...");
try {
@@ -77,7 +98,7 @@ export function useLoading() {
throw error;
}
},
- [isLoading, startLoading, stopLoading],
+ [startLoading, stopLoading],
);
return {
@@ -94,7 +115,7 @@ export function useListLoading() {
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
- const runInitial = useCallback(async (task: () => Promise) => {
+ const runInitial = useCallback(async (task: () => Promise) => {
setLoading(true);
try {
return await task();
@@ -103,7 +124,7 @@ export function useListLoading() {
}
}, []);
- const runRefresh = useCallback(async (task: () => Promise) => {
+ const runRefresh = useCallback(async (task: () => Promise) => {
setRefreshing(true);
try {
return await task();
@@ -112,7 +133,7 @@ export function useListLoading() {
}
}, []);
- const runLoadMore = useCallback(async (task: () => Promise) => {
+ const runLoadMore = useCallback(async (task: () => Promise) => {
setLoadingMore(true);
try {
return await task();
diff --git a/code/app/app-rn/src/store/favorite.ts b/code/app/app-rn/src/store/favorite.ts
index bf50cd4f..121ffb59 100644
--- a/code/app/app-rn/src/store/favorite.ts
+++ b/code/app/app-rn/src/store/favorite.ts
@@ -1,9 +1,5 @@
import { create } from "zustand";
-import {
- loadDataApi,
- saveDataApi,
- getUniqueIdApi,
-} from "@/request/api";
+import { loadDataApi, saveDataApi, getUniqueIdApi } from "@/request/api";
import { useUserStore } from "./user";
interface FavoriteState {
diff --git a/code/app/app-rn/src/store/finance.ts b/code/app/app-rn/src/store/finance.ts
index 8149d289..0028319b 100644
--- a/code/app/app-rn/src/store/finance.ts
+++ b/code/app/app-rn/src/store/finance.ts
@@ -21,8 +21,10 @@ export const useFinanceStore = create((set, get) => ({
clearCategories: () => set({ categories: [] }),
getCategories: () => get().categories,
- getExpenseCategories: () => get().categories.filter((category) => category.type === FinanceType.Expense),
- getIncomeCategories: () => get().categories.filter((category) => category.type === FinanceType.Income),
+ getExpenseCategories: () =>
+ get().categories.filter((category) => category.type === FinanceType.Expense),
+ getIncomeCategories: () =>
+ get().categories.filter((category) => category.type === FinanceType.Income),
loadCategories: async (familyId: number) => {
if (!familyId) return;
diff --git a/code/app/app-rn/src/uniwind-types.d.ts b/code/app/app-rn/src/uniwind-types.d.ts
index cc099419..14402dbc 100644
--- a/code/app/app-rn/src/uniwind-types.d.ts
+++ b/code/app/app-rn/src/uniwind-types.d.ts
@@ -1,10 +1,10 @@
// NOTE: This file is generated by uniwind and it should not be edited manually.
///
-declare module 'uniwind' {
- export interface UniwindConfig {
- themes: readonly ['light', 'dark']
- }
+declare module "uniwind" {
+ export interface UniwindConfig {
+ themes: readonly ["light", "dark"];
+ }
}
-export {}
+export {};
diff --git a/code/app/app-rn/tsconfig.json b/code/app/app-rn/tsconfig.json
index d8870c52..afa1230b 100644
--- a/code/app/app-rn/tsconfig.json
+++ b/code/app/app-rn/tsconfig.json
@@ -3,12 +3,8 @@
"compilerOptions": {
"strict": true,
"paths": {
- "@/*": [
- "./src/*"
- ],
- "@/assets/*": [
- "./assets/*"
- ]
+ "@/*": ["./src/*"],
+ "@/assets/*": ["./assets/*"]
}
},
"include": [
@@ -17,6 +13,7 @@
"./src/uniwind-types.d.ts",
"env.js",
".expo/types/**/*.ts",
- "expo-env.d.ts"
-, "src/app/auth/login_2" ]
+ "expo-env.d.ts",
+ "src/app/auth/login_2"
+ ]
}