From e687846eb4fa8aad16b568ba31281bab975443be Mon Sep 17 00:00:00 2001 From: oneao Date: Sun, 28 Jun 2026 21:56:23 +0800 Subject: [PATCH] 20260628215623 --- code/app/.workbuddy/memory/2026-06-27.md | 55 ++ code/app/.workbuddy/memory/2026-06-28.md | 20 + code/app/.workbuddy/memory/MEMORY.md | 9 + code/app/app-rn/.env.development | 2 +- code/app/app-rn/.env.production | 2 +- code/app/app-rn/.oxlintrc.json | 8 +- code/app/app-rn/app.json | 6 +- code/app/app-rn/assets/expo.icon/icon.json | 47 +- code/app/app-rn/babel.config.js | 2 +- code/app/app-rn/package.json | 9 +- .../app-rn/plugins/withAndroidSignature.js | 41 +- code/app/app-rn/pnpm-lock.yaml | 43 +- code/app/app-rn/pnpm-workspace.yaml | 2 + .../src/app/(main)/(tabs)/chat/index.tsx | 351 ++++---- .../src/app/(main)/(tabs)/home/index.tsx | 19 +- .../src/app/(main)/(tabs)/space/index.tsx | 51 +- code/app/app-rn/src/app/(main)/auth/login.tsx | 12 +- .../app-rn/src/app/(main)/chat/history.tsx | 22 +- .../app-rn/src/app/(main)/family/entry.tsx | 27 +- .../app-rn/src/app/(main)/family/settings.tsx | 40 +- .../app/app-rn/src/app/(main)/family/user.tsx | 35 +- .../app/app-rn/src/app/(main)/tools/index.tsx | 52 +- code/app/app-rn/src/app/_layout.tsx | 27 +- .../app-rn/src/app/finance/(tabs)/_layout.tsx | 6 +- .../src/app/finance/(tabs)/analysis/index.tsx | 816 +++++++++++++----- .../src/app/finance/(tabs)/home/index.tsx | 178 ++-- code/app/app-rn/src/app/finance/add/index.tsx | 118 +-- .../app-rn/src/app/finance/category/index.tsx | 24 +- .../app-rn/src/app/finance/detail/[id].tsx | 53 +- .../app/app-rn/src/components/BottomSheet.tsx | 182 ++++ code/app/app-rn/src/components/DateTime.tsx | 16 +- .../app-rn/src/components/DateTimeRange.tsx | 7 +- .../app/app-rn/src/components/EmptyStatus.tsx | 8 +- .../app-rn/src/components/LoadingOverlay.tsx | 5 +- .../components/iconfont/IconAppClothes.tsx | 44 +- .../components/iconfont/IconAppFinance.tsx | 36 +- .../components/iconfont/IconFinanceCanyin.tsx | 62 +- .../iconfont/IconFinanceChongwu.tsx | 52 +- .../components/iconfont/IconFinanceGongzi.tsx | 38 +- .../components/iconfont/IconFinanceGouwu.tsx | 48 +- .../iconfont/IconFinanceJianzhi.tsx | 38 +- .../iconfont/IconFinanceJiaotong.tsx | 52 +- .../components/iconfont/IconFinanceLijin.tsx | 30 +- .../iconfont/IconFinanceLingshi.tsx | 52 +- .../components/iconfont/IconFinanceShicai.tsx | 54 +- .../components/iconfont/IconFinanceShouru.tsx | 30 +- .../iconfont/IconFinanceShuidian.tsx | 30 +- .../components/iconfont/IconFinanceShuma.tsx | 44 +- .../components/iconfont/IconFinanceYifu.tsx | 46 +- .../components/iconfont/IconFinanceYiliao.tsx | 56 +- .../components/iconfont/IconFinanceYouxi.tsx | 290 +++---- .../components/iconfont/IconFinanceYule.tsx | 42 +- .../iconfont/IconFinanceYundong.tsx | 34 +- .../components/iconfont/IconFinanceZhichu.tsx | 30 +- .../iconfont/IconFinanceZhufang.tsx | 35 +- .../app-rn/src/components/iconfont/IconQq.tsx | 42 +- .../src/components/iconfont/IconWeixin.tsx | 26 +- .../app-rn/src/components/iconfont/helper.ts | 14 +- .../app-rn/src/components/iconfont/index.tsx | 169 ++-- .../app-rn/src/components/layout/tabbar.tsx | 6 +- .../app/app-rn/src/components/layout/view.tsx | 24 +- code/app/app-rn/src/global.css | 2 +- .../app/app-rn/src/hooks/useChatWebSocket.tsx | 22 +- code/app/app-rn/src/hooks/useLoading.ts | 47 +- code/app/app-rn/src/store/favorite.ts | 6 +- code/app/app-rn/src/store/finance.ts | 6 +- code/app/app-rn/src/uniwind-types.d.ts | 10 +- code/app/app-rn/tsconfig.json | 13 +- 68 files changed, 2234 insertions(+), 1591 deletions(-) create mode 100644 code/app/.workbuddy/memory/2026-06-27.md create mode 100644 code/app/.workbuddy/memory/2026-06-28.md create mode 100644 code/app/app-rn/pnpm-workspace.yaml create mode 100644 code/app/app-rn/src/components/BottomSheet.tsx 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/(main)/family/user.tsx b/code/app/app-rn/src/app/(main)/family/user.tsx index 4cc772d7..befc0f00 100644 --- a/code/app/app-rn/src/app/(main)/family/user.tsx +++ b/code/app/app-rn/src/app/(main)/family/user.tsx @@ -8,12 +8,11 @@ import { ListGroup, Separator, Button, - BottomSheet, Dialog, - Input, Label, TextField, } from "heroui-native"; +import BottomSheet from "@/components/BottomSheet"; import { useLoading } from "@/hooks/useLoading"; import { LoadingOverlay } from "@/components/LoadingOverlay"; import { loadDataApi, saveDataApi } from "@/request/api"; @@ -84,9 +83,13 @@ export default function UserSetting() { useEffect(() => { if (showNameSheet) { - requestAnimationFrame(() => { + // BottomSheetModal.present() 是异步的,且有 300ms 展开动画, + // requestAnimationFrame 只等 1 帧不够,input 可能还没挂载。 + // 等 350ms(略大于动画时长)再聚焦,确保 sheet 已展开、input 已就绪。 + const timer = setTimeout(() => { nameInputRef.current?.focus(); - }); + }, 300); + return () => clearTimeout(timer); } else { Keyboard.dismiss(); } @@ -94,9 +97,10 @@ export default function UserSetting() { useEffect(() => { if (showPhoneSheet) { - requestAnimationFrame(() => { + const timer = setTimeout(() => { phoneInputRef.current?.focus(); - }); + }, 300); + return () => clearTimeout(timer); } else { Keyboard.dismiss(); } @@ -504,11 +508,7 @@ export default function UserSetting() { return ( - + @@ -559,7 +559,9 @@ export default function UserSetting() { 姓名 - {userInfo.name || "-"} + + {userInfo.name || "-"} + @@ -580,7 +582,9 @@ export default function UserSetting() { 手机号 - {userInfo.phone || "-"} + + {userInfo.phone || "-"} + @@ -723,7 +727,7 @@ export default function UserSetting() { - setRenameForm({ name })} @@ -747,12 +751,11 @@ export default function UserSetting() { - setPhoneForm({ phone })} placeholder="请输入手机号" - inputMode="tel" onSubmitEditing={handlePhone} returnKeyType="done" /> diff --git a/code/app/app-rn/src/app/(main)/tools/index.tsx b/code/app/app-rn/src/app/(main)/tools/index.tsx index 72a6228b..f517c9c2 100644 --- a/code/app/app-rn/src/app/(main)/tools/index.tsx +++ b/code/app/app-rn/src/app/(main)/tools/index.tsx @@ -80,24 +80,18 @@ export default function Tools() { return result; }, [keyword, favoriteStore.favorites]); - const handleLetterPress = useCallback( - (letter: string) => { - const y = headerPositions.current[letter]; - if (y !== undefined && scrollRef.current) { - scrollRef.current.scrollTo({ y, animated: true }); - } - }, - [] - ); + const handleLetterPress = useCallback((letter: string) => { + const y = headerPositions.current[letter]; + if (y !== undefined && scrollRef.current) { + scrollRef.current.scrollTo({ y, animated: true }); + } + }, []); return ( - + {sortedApps.length === 0 ? ( ) : ( @@ -109,11 +103,9 @@ export default function Tools() { { - headerPositions.current[headerKey] = - e.nativeEvent.layout.y; + headerPositions.current[headerKey] = e.nativeEvent.layout.y; if (!keyword) { - headerPositions.current[item.title] = - e.nativeEvent.layout.y; + headerPositions.current[item.title] = e.nativeEvent.layout.y; } }} > @@ -143,7 +135,10 @@ export default function Tools() { const isFav = favoriteStore.isFavorite(subItem.id); return ( - + router.navigate(subItem.id as any)} className="flex-row items-center py-3.5 px-4" @@ -154,7 +149,10 @@ export default function Tools() { {subItem.title} {subItem.description ? ( - + {subItem.description} ) : null} @@ -164,14 +162,13 @@ export default function Tools() { {/* 收藏 — 行右侧,独立触摸 */} { - const result = - await favoriteStore.toggleFavorite( - familyStore.currentFamily.family_id, - subItem.id - ); - toast(result ? "已收藏" : "已取消收藏"); - }} + onPress={async () => { + const result = await favoriteStore.toggleFavorite( + familyStore.currentFamily.family_id, + subItem.id, + ); + toast(result ? "已收藏" : "已取消收藏"); + }} className="absolute right-4 top-0 bottom-0 justify-center px-2 z-10" > - ); } 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" + /> + + + + + + + + + + + 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() { }} /> - { if (!open) closeActionMenu(); }}> + { + if (!open) closeActionMenu(); + }} + > diff --git a/code/app/app-rn/src/app/finance/add/index.tsx b/code/app/app-rn/src/app/finance/add/index.tsx index 007d2370..afc5faf1 100644 --- a/code/app/app-rn/src/app/finance/add/index.tsx +++ b/code/app/app-rn/src/app/finance/add/index.tsx @@ -3,7 +3,8 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import PageLayout from "@/layouts/PageLayout"; import { useState, useMemo, useCallback, memo, useEffect, useRef } from "react"; import LayoutView from "@/components/layout/view"; -import { Tabs, Button, BottomSheet, Input, Label, TextField, PressableFeedback } from "heroui-native"; +import { Tabs, Button, Label, TextField, PressableFeedback } from "heroui-native"; +import BottomSheet from "@/components/BottomSheet"; import { useFinanceStore } from "@/store/finance"; import { useFamilyStore } from "@/store"; import { getUniqueIdApi, loadDataApi, saveDataApi } from "@/request/api"; @@ -16,7 +17,7 @@ import { SearchIcon, ChevronRight, Settings2Icon } from "lucide-react-native"; import FinanceCategoryIcon from "@/components/finance/FinanceCategoryIcon"; import { FinanceCategory, FinanceType } from "@/types/finance"; -// ─── 分类项(memo 避免选中状态变化时全量重渲染)───────── +// —— 分类项(memo 避免选中状态变化时全量重渲染)—— const CategoryItem = memo(function CategoryItem({ item, isActive, @@ -37,11 +38,7 @@ const CategoryItem = memo(function CategoryItem({ isActive ? "bg-accent" : "bg-gray-100" }`} > - + (); @@ -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 */} - - + {/* 按钮 */} - {!isEdit && ( - )} @@ -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" + ] }