From 72571b2b9d8060602d3e52c735fcd19252a8fe4f Mon Sep 17 00:00:00 2001 From: 12975 <1297598740@qq.com> Date: Wed, 28 Jan 2026 01:03:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=92=8C=E8=B4=A6=E6=88=B7=E5=88=97=E8=A1=A8=E5=8F=8A=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=EF=BC=8C=E5=B9=B6=E6=96=B0=E5=A2=9E=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E9=80=89=E6=8B=A9=E5=99=A8=E7=BB=84=E4=BB=B6=E5=92=8C?= =?UTF-8?q?=E4=B8=BB=E6=A0=87=E7=AD=BE=E5=AF=BC=E8=88=AA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 2 + package-lock.json | 278 +++++++++++++++++- package.json | 7 +- src/components/common/AccountSelector.tsx | 28 +- src/components/common/AppIcon.tsx | 21 ++ src/components/common/CategorySelector.tsx | 70 +++-- src/components/common/IconSelector.tsx | 30 +- src/components/common/index.ts | 2 + src/navigation/MainTab.tsx | 43 +-- src/screens/Accounts/AccountDetailScreen.tsx | 17 +- src/screens/Accounts/AccountsScreen.tsx | 36 ++- .../Transactions/TransactionDetailScreen.tsx | 38 ++- .../Transactions/TransactionsScreen.tsx | 11 +- src/utils/format.ts | 20 +- task.md | 44 ++- 15 files changed, 519 insertions(+), 128 deletions(-) create mode 100644 src/components/common/AppIcon.tsx diff --git a/android/app/build.gradle b/android/app/build.gradle index 1d3509b..7d6f979 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -117,3 +117,5 @@ dependencies { implementation jscFlavor } } + +apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" diff --git a/package-lock.json b/package-lock.json index 40dfe0c..a5e69d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,16 @@ "@react-navigation/bottom-tabs": "^7.10.1", "@react-navigation/native": "^7.1.28", "@react-navigation/native-stack": "^7.11.0", + "@types/react-native-vector-icons": "^6.4.18", "react": "19.2.0", "react-native": "0.83.1", "react-native-gesture-handler": "^2.30.0", + "react-native-gifted-charts": "^1.4.70", + "react-native-linear-gradient": "^2.8.3", "react-native-safe-area-context": "^5.5.2", - "react-native-screens": "^4.20.0" + "react-native-screens": "^4.20.0", + "react-native-svg": "^15.15.1", + "react-native-vector-icons": "^10.3.0" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -3603,12 +3608,30 @@ "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-native": { + "version": "0.70.19", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz", + "integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-native-vector-icons": { + "version": "6.4.18", + "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz", + "integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@types/react-native": "^0.70" + } + }, "node_modules/@types/react-test-renderer": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", @@ -4550,6 +4573,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -5159,11 +5188,51 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -5396,6 +5465,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5451,6 +5575,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -6707,6 +6843,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gifted-charts-core": { + "version": "0.1.72", + "resolved": "https://registry.npmjs.org/gifted-charts-core/-/gifted-charts-core-0.1.72.tgz", + "integrity": "sha512-ID98gSvYA/6Egg/SxjjVAnFMpK0c4H9NZKkF4vRWtvJOioWegEGlQKN7BcQhuyKHr/HPmz7kGID3XJUuoM7UTQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-svg": "*" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8833,6 +8980,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -9394,6 +9547,18 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -9416,7 +9581,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9941,7 +10105,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -9953,7 +10116,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/punycode": { @@ -10203,6 +10365,40 @@ "react-native": "*" } }, + "node_modules/react-native-gifted-charts": { + "version": "1.4.70", + "resolved": "https://registry.npmjs.org/react-native-gifted-charts/-/react-native-gifted-charts-1.4.70.tgz", + "integrity": "sha512-nsbth3aMeMwu40ZNAn512TGZLks5Untplby5ABSe22+FLUdXYkdCzeR5/CodL6gxCcpsqQYXgAVvtujZnDRErA==", + "license": "MIT", + "dependencies": { + "gifted-charts-core": "0.1.72" + }, + "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 + } + } + }, + "node_modules/react-native-linear-gradient": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz", + "integrity": "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", @@ -10227,6 +10423,76 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.15.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.1.tgz", + "integrity": "sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-vector-icons": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz", + "integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==", + "deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2", + "yargs": "^16.1.1" + }, + "bin": { + "fa-upgrade.sh": "bin/fa-upgrade.sh", + "fa5-upgrade": "bin/fa5-upgrade.sh", + "fa6-upgrade": "bin/fa6-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-vector-icons/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", diff --git a/package.json b/package.json index c78214c..ca02c1d 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,16 @@ "@react-navigation/bottom-tabs": "^7.10.1", "@react-navigation/native": "^7.1.28", "@react-navigation/native-stack": "^7.11.0", + "@types/react-native-vector-icons": "^6.4.18", "react": "19.2.0", "react-native": "0.83.1", "react-native-gesture-handler": "^2.30.0", + "react-native-gifted-charts": "^1.4.70", + "react-native-linear-gradient": "^2.8.3", "react-native-safe-area-context": "^5.5.2", - "react-native-screens": "^4.20.0" + "react-native-screens": "^4.20.0", + "react-native-svg": "^15.15.1", + "react-native-vector-icons": "^10.3.0" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/src/components/common/AccountSelector.tsx b/src/components/common/AccountSelector.tsx index ad82ca5..66ba8d1 100644 --- a/src/components/common/AccountSelector.tsx +++ b/src/components/common/AccountSelector.tsx @@ -16,6 +16,7 @@ import { useTheme } from '../../contexts'; import { accountService } from '../../services'; import { spacing, borderRadius, typography } from '../../theme'; import type { Account } from '../../types'; +import { AppIcon } from './AppIcon'; interface AccountSelectorProps { value?: number; @@ -33,12 +34,12 @@ interface AccountSelectorProps { // 账户类型图标映射 const ACCOUNT_ICONS: Record = { - cash: '💵', - debit_card: '💳', - credit_card: '💳', - e_wallet: '📱', - credit_line: '🏦', - investment: '📈', + cash: 'cash', + debit_card: 'credit-card', + credit_card: 'credit-card-multiple', + e_wallet: 'cellphone-check', + credit_line: 'bank', + investment: 'chart-line', }; export function AccountSelector({ @@ -119,7 +120,7 @@ export function AccountSelector({ if (account.icon && !account.icon.includes(':')) { return account.icon; } - return ACCOUNT_ICONS[account.type] || '💰'; + return ACCOUNT_ICONS[account.type] || 'wallet'; }; const formatBalance = (balance: number) => { @@ -158,7 +159,11 @@ export function AccountSelector({ activeOpacity={0.7} > - {getAccountIcon(item)} + {item.name} @@ -195,7 +200,12 @@ export function AccountSelector({ > {selectedAccount ? ( - {getAccountIcon(selectedAccount)} + {selectedAccount.name} {formatBalance(selectedAccount.balance)} diff --git a/src/components/common/AppIcon.tsx b/src/components/common/AppIcon.tsx new file mode 100644 index 0000000..681d38c --- /dev/null +++ b/src/components/common/AppIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Text, TextStyle } from 'react-native'; // Removed StyleProp +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; + +interface AppIconProps { + name: string; + size?: number; + color?: string; + style?: TextStyle; // Simplification +} + +export const AppIcon: React.FC = ({ name, size = 24, color, style }) => { + // Check if likely emoji (non-ascii) + const isEmoji = /[^\u0000-\u007F]/.test(name); + + if (isEmoji) { + return {name}; + } + + return ; +}; diff --git a/src/components/common/CategorySelector.tsx b/src/components/common/CategorySelector.tsx index 9480868..e0af4a8 100644 --- a/src/components/common/CategorySelector.tsx +++ b/src/components/common/CategorySelector.tsx @@ -16,11 +16,12 @@ import { useTheme } from '../../contexts'; import { categoryService } from '../../services'; import { spacing, borderRadius, typography } from '../../theme'; import type { Category, TransactionType } from '../../types'; +import { AppIcon } from './AppIcon'; interface CategorySelectorProps { value?: number; onChange?: (categoryId: number, category: Category) => void; - type?: TransactionType; // Made optional as it might be inferred or not needed if categories passed + type?: TransactionType; disabled?: boolean; // Modal Mode props @@ -34,33 +35,33 @@ interface CategorySelectorProps { // 默认分类图标映射 const CATEGORY_ICONS: Record = { // 支出分类 - 餐饮: '🍔', - 交通: '🚗', - 购物: '🛒', - 娱乐: '🎮', - 医疗: '💊', - 教育: '📚', - 居住: '🏠', - 通讯: '📱', - 服饰: '👔', - 美容: '💄', - 运动: '⚽', - 旅行: '✈️', - 宠物: '🐱', - 社交: '👥', - 礼物: '🎁', - 办公: '💼', - 数码: '💻', - 其他支出: '📦', + 餐饮: 'food', + 交通: 'car', + 购物: 'shopping', + 娱乐: 'gamepad-variant', + 医疗: 'pill', + 教育: 'school', + 居住: 'home', + 通讯: 'cellphone', + 服饰: 'tshirt-crew', + 美容: 'lipstick', + 运动: 'soccer', + 旅行: 'airplane', + 宠物: 'cat', + 社交: 'account-group', + 礼物: 'gift', + 办公: 'briefcase', + 数码: 'laptop', + 其他支出: 'package-variant', // 收入分类 - 工资: '💰', - 奖金: '🎉', - 投资: '📈', - 兼职: '💼', - 理财: '🏦', - 红包: '🧧', - 报销: '📋', - 其他收入: '💵', + 工资: 'cash-multiple', + 奖金: 'party-popper', + 投资: 'chart-line', + 兼职: 'briefcase-clock', + 理财: 'bank', + 红包: 'email-heart', + 报销: 'clipboard-check', + 其他收入: 'cash-plus', }; export function CategorySelector({ @@ -134,7 +135,7 @@ export function CategorySelector({ return category.icon; } // 否则使用默认图标映射 - return CATEGORY_ICONS[category.name] || (type === 'expense' ? '💸' : '💰'); + return CATEGORY_ICONS[category.name] || (type === 'expense' ? 'cash-minus' : 'cash-plus'); }; const styles = createStyles(colors, isDark); @@ -175,7 +176,11 @@ export function CategorySelector({ currentId === item.id && styles.categoryIconSelected, ]} > - {getCategoryIcon(item)} + {selectedCategory ? ( - {getCategoryIcon(selectedCategory)} + {selectedCategory.name} ) : ( diff --git a/src/components/common/IconSelector.tsx b/src/components/common/IconSelector.tsx index c28207c..7c62eda 100644 --- a/src/components/common/IconSelector.tsx +++ b/src/components/common/IconSelector.tsx @@ -14,6 +14,7 @@ import { } from 'react-native'; import { useTheme } from '../../contexts'; import { spacing, borderRadius, typography } from '../../theme'; +import { AppIcon } from './AppIcon'; interface IconSelectorProps { visible: boolean; @@ -27,43 +28,43 @@ interface IconSelectorProps { const ICON_CATEGORIES = [ { name: '通用', - icons: ['💰', '💵', '💴', '💶', '💷', '💸', '🏦', '🏧', '💳', '💎'], + icons: ['cash', 'cash-multiple', 'currency-usd', 'currency-eur', 'credit-card', 'bank', 'wallet', 'piggy-bank', 'chart-line', 'safe'], }, { name: '餐饮', - icons: ['🍔', '🍕', '🍜', '🍣', '🍱', '☕', '🍺', '🍰', '🍎', '🥗'], + icons: ['food', 'food-outline', 'coffee', 'cup', 'glass-cocktail', 'cake', 'pizza', 'hamburger', 'noodles', 'fish'], }, { name: '购物', - icons: ['🛒', '🛍️', '👗', '👟', '💄', '🎁', '📱', '💻', '🎮', '📚'], + icons: ['cart', 'shopping', 'tshirt-crew', 'shoe-sneaker', 'lipstick', 'gift', 'cellphone', 'laptop', 'gamepad-variant', 'book-open'], }, { name: '交通', - icons: ['🚗', '🚌', '🚇', '✈️', '🚂', '🚕', '⛽', '🚲', '🛵', '🚀'], + icons: ['car', 'bus', 'train', 'airplane', 'taxi', 'gas-station', 'bike', 'moped', 'rocket', 'map-marker'], }, { name: '居家', - icons: ['🏠', '🛋️', '🛏️', '🚿', '💡', '🔧', '🧹', '🧺', '🪴', '🏡'], + icons: ['home', 'sofa', 'bed', 'shower', 'lightbulb', 'wrench', 'broom', 'washing-machine', 'flower', 'home-account'], }, { name: '娱乐', - icons: ['🎬', '🎵', '🎮', '🎯', '🎨', '📷', '🎤', '🎭', '🎪', '🎢'], + icons: ['movie', 'music', 'controller-classic', 'target', 'palette', 'camera', 'microphone', 'drama-masks', 'ticket', 'ferris-wheel'], }, { name: '健康', - icons: ['💊', '🏥', '🩺', '💉', '🧘', '🏃', '🚴', '⚽', '🏀', '🎾'], + icons: ['pill', 'hospital-building', 'stethoscope', 'needle', 'yoga', 'run', 'bike-fast', 'soccer', 'basketball', 'tennis'], }, { name: '教育', - icons: ['📚', '🎓', '✏️', '📐', '🔬', '🧪', '📝', '💼', '🖥️', '📖'], + icons: ['school', 'book-open-page-variant', 'pencil', 'ruler-square', 'microscope', 'flask', 'clipboard-text', 'briefcase', 'monitor', 'brain'], }, { name: '社交', - icons: ['🎂', '🎉', '💐', '🍾', '🎊', '💌', '👨‍👩‍👧', '👥', '🤝', '❤️'], + icons: ['cake-variant', 'party-popper', 'flower-tulip', 'glass-wine', 'cards-playing', 'email-heart', 'account-group', 'account-multiple', 'handshake', 'heart'], }, { name: '其他', - icons: ['📦', '🔑', '🎫', '📬', '🗂️', '📎', '✂️', '🔒', '⭐', '❓'], + icons: ['package-variant', 'key', 'ticket-confirmation', 'mailbox', 'folder', 'paperclip', 'scissors', 'lock', 'star', 'help-circle'], }, ]; @@ -144,7 +145,11 @@ export default function IconSelector({ ]} onPress={() => handleSelect(item)} > - {item} + )} /> @@ -226,7 +231,4 @@ const createStyles = (colors: any, isDark: boolean) => borderWidth: 2, borderColor: colors.primary[500], }, - iconEmoji: { - fontSize: 28, - }, }); diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 687b62a..246e81c 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -7,3 +7,5 @@ export { AccountSelector } from './AccountSelector'; export { default as DatePickerModal } from './DatePickerModal'; export { default as AmountInput } from './AmountInput'; export { default as IconSelector } from './IconSelector'; +export { AppIcon } from './AppIcon'; + diff --git a/src/navigation/MainTab.tsx b/src/navigation/MainTab.tsx index 106bf48..28ad6d0 100644 --- a/src/navigation/MainTab.tsx +++ b/src/navigation/MainTab.tsx @@ -9,6 +9,7 @@ import { useNavigation } from '@react-navigation/native'; import type { MainTabParamList } from './types'; import { useTheme } from '../contexts'; import { spacing } from '../theme'; +import { AppIcon } from '../components/common/AppIcon'; // 页面组件 import HomeScreen from '../screens/Home/HomeScreen'; @@ -21,17 +22,21 @@ const Tab = createBottomTabNavigator(); // 简单的图标组件 function TabIcon({ name, focused }: { name: string; focused: boolean }) { const iconMap: Record = { - Home: '🏠', - Transactions: '📝', - AddTransaction: '➕', - Accounts: '💰', - Settings: '⚙️', + Home: focused ? 'home' : 'home-outline', + Transactions: focused ? 'file-document' : 'file-document-outline', + AddTransaction: 'plus', + Accounts: focused ? 'wallet' : 'wallet-outline', + Settings: focused ? 'cog' : 'cog-outline', }; + const color = focused ? '#1a73e8' : '#666666'; // Will be overridden by tabBarActiveTintColor + return ( - - {iconMap[name] || '📱'} - + ); } @@ -50,7 +55,7 @@ function AddButton() { style={[styles.addButton, { backgroundColor: colors.primary[500] }]} activeOpacity={0.8} > - + + ); } @@ -76,8 +81,18 @@ export default function MainTabNavigator() { }, tabBarActiveTintColor: colors.primary[500], tabBarInactiveTintColor: colors.textSecondary, - tabBarIcon: ({ focused }) => ( - + tabBarIcon: ({ focused, color }) => ( + ), tabBarLabelStyle: { fontSize: 11, @@ -131,10 +146,4 @@ const styles = StyleSheet.create({ shadowRadius: 8, elevation: 8, }, - addButtonText: { - color: '#FFFFFF', - fontSize: 32, - fontWeight: '300', - marginTop: -2, - }, }); diff --git a/src/screens/Accounts/AccountDetailScreen.tsx b/src/screens/Accounts/AccountDetailScreen.tsx index b0057da..aea367f 100644 --- a/src/screens/Accounts/AccountDetailScreen.tsx +++ b/src/screens/Accounts/AccountDetailScreen.tsx @@ -23,6 +23,7 @@ import { spacing, borderRadius, typography } from '../../theme'; import { formatCurrency, formatDate, getAccountTypeLabel } from '../../utils/format'; import type { Account, Transaction } from '../../types'; import type { RootStackParamList } from '../../navigation/types'; +import { AppIcon } from '../../components/common/AppIcon'; type RouteProps = RouteProp; type NavigationProp = NativeStackNavigationProp; @@ -118,7 +119,11 @@ export default function AccountDetailScreen() { onPress={() => navigation.navigate('TransactionDetail', { transactionId: item.id })} > - {item.categoryIcon || '📦'} + {item.categoryName} @@ -158,7 +163,7 @@ export default function AccountDetailScreen() { style={styles.backButton} onPress={() => navigation.goBack()} > - + 账户详情 @@ -180,7 +185,11 @@ export default function AccountDetailScreen() { {/* 账户卡片 */} - {account.icon || '💳'} + {account.name} {getAccountTypeLabel(account.type)} @@ -229,7 +238,7 @@ export default function AccountDetailScreen() { /> ) : ( - 📝 + 暂无交易记录 )} diff --git a/src/screens/Accounts/AccountsScreen.tsx b/src/screens/Accounts/AccountsScreen.tsx index 62252bb..37e5dff 100644 --- a/src/screens/Accounts/AccountsScreen.tsx +++ b/src/screens/Accounts/AccountsScreen.tsx @@ -19,6 +19,7 @@ import { accountService } from '../../services'; import { spacing, borderRadius, typography } from '../../theme'; import type { Account, AssetOverview } from '../../types'; import type { RootStackParamList } from '../../navigation/types'; +import { AppIcon } from '../../components/common/AppIcon'; type NavigationProp = NativeStackNavigationProp; @@ -79,14 +80,14 @@ export default function AccountsScreen() { const getAccountIcon = (type: string) => { const icons: Record = { - cash: '💵', - debit_card: '💳', - credit_card: '💳', - e_wallet: '📱', - credit_line: '🏦', - investment: '📈', + cash: 'cash', + debit_card: 'credit-card', + credit_card: 'credit-card-multiple', + e_wallet: 'cellphone-check', + credit_line: 'bank', + investment: 'chart-line', }; - return icons[type] || '💰'; + return icons[type] || 'wallet'; }; // 分组账户 @@ -105,13 +106,15 @@ export default function AccountsScreen() { style={styles.transferButton} onPress={() => navigation.navigate('Transfer')} > - 🔄 转账 + + 转账 navigation.navigate('AddAccount')} > - + 添加 + + 添加 @@ -163,7 +166,11 @@ export default function AccountsScreen() { onPress={() => navigation.navigate('AccountDetail', { accountId: account.id })} > - {getAccountIcon(account.type)} + {account.name} @@ -191,7 +198,11 @@ export default function AccountsScreen() { onPress={() => navigation.navigate('AccountDetail', { accountId: account.id })} > - {getAccountIcon(account.type)} + {account.name} @@ -209,7 +220,7 @@ export default function AccountsScreen() { {/* 空状态 */} {accounts.length === 0 && ( - 💳 + 暂无账户 点击右上角添加账户 @@ -219,6 +230,7 @@ export default function AccountsScreen() { ); } + const createStyles = (colors: any, isDark: boolean, insets: any) => StyleSheet.create({ container: { diff --git a/src/screens/Transactions/TransactionDetailScreen.tsx b/src/screens/Transactions/TransactionDetailScreen.tsx index 92fbf6b..a46b215 100644 --- a/src/screens/Transactions/TransactionDetailScreen.tsx +++ b/src/screens/Transactions/TransactionDetailScreen.tsx @@ -21,14 +21,15 @@ import { spacing, borderRadius, typography } from '../../theme'; import { formatCurrency, formatDate, formatRelativeTime } from '../../utils/format'; import type { Transaction } from '../../types'; import type { RootStackParamList } from '../../navigation/types'; +import { AppIcon } from '../../components/common/AppIcon'; type RouteProps = RouteProp; type NavigationProp = NativeStackNavigationProp; const TYPE_CONFIG = { - income: { label: '收入', icon: '💰', colorKey: 'income' as const }, - expense: { label: '支出', icon: '💸', colorKey: 'expense' as const }, - transfer: { label: '转账', icon: '🔄', colorKey: 'transfer' as const }, + income: { label: '收入', icon: 'cash-plus', colorKey: 'income' as const }, + expense: { label: '支出', icon: 'cash-minus', colorKey: 'expense' as const }, + transfer: { label: '转账', icon: 'swap-horizontal', colorKey: 'transfer' as const }, }; export default function TransactionDetailScreen() { @@ -114,7 +115,7 @@ export default function TransactionDetailScreen() { style={styles.backButton} onPress={() => navigation.goBack()} > - + 交易详情 @@ -131,7 +132,7 @@ export default function TransactionDetailScreen() { {/* 金额卡片 */} - {typeConfig.icon} + {transaction.type === 'income' ? '+' : '-'} @@ -146,9 +147,12 @@ export default function TransactionDetailScreen() { label="分类" value={ - - {transaction.categoryIcon || '📦'} - + {transaction.categoryName} @@ -161,9 +165,12 @@ export default function TransactionDetailScreen() { label="账户" value={ - - {transaction.accountIcon || '💳'} - + {transaction.accountName} @@ -177,9 +184,12 @@ export default function TransactionDetailScreen() { label="转入账户" value={ - - {transaction.toAccountIcon || '💳'} - + {transaction.toAccountName} diff --git a/src/screens/Transactions/TransactionsScreen.tsx b/src/screens/Transactions/TransactionsScreen.tsx index bad65eb..c1df2cd 100644 --- a/src/screens/Transactions/TransactionsScreen.tsx +++ b/src/screens/Transactions/TransactionsScreen.tsx @@ -20,6 +20,7 @@ import { transactionService } from '../../services'; import { spacing, borderRadius, typography } from '../../theme'; import type { Transaction } from '../../types'; import type { RootStackParamList } from '../../navigation/types'; +import { AppIcon } from '../../components/common/AppIcon'; type NavigationProp = NativeStackNavigationProp; @@ -105,9 +106,11 @@ export default function TransactionsScreen() { onPress={() => navigation.navigate('TransactionDetail', { transactionId: item.id })} > - - {item.categoryIcon || (item.type === 'income' ? '💰' : item.type === 'expense' ? '💸' : '🔄')} - + @@ -155,7 +158,7 @@ export default function TransactionsScreen() { onEndReachedThreshold={0.3} ListEmptyComponent={ - 📝 + 暂无交易记录 } diff --git a/src/utils/format.ts b/src/utils/format.ts index ba9eaec..ed7acbd 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -98,11 +98,11 @@ export function getTransactionTypeColor(type: string, colors: any): string { export function getTransactionTypeIcon(type: string): string { switch (type) { case 'income': - return '💰'; + return 'cash-plus'; case 'expense': - return '💸'; + return 'cash-minus'; default: - return '🔄'; + return 'swap-horizontal'; } } @@ -126,12 +126,12 @@ export function getAccountTypeLabel(type: string): string { */ export function getAccountTypeIcon(type: string): string { const icons: Record = { - cash: '💵', - debit_card: '💳', - credit_card: '💳', - e_wallet: '📱', - credit_line: '🏦', - investment: '📈', + cash: 'cash', + debit_card: 'credit-card', + credit_card: 'credit-card-multiple', + e_wallet: 'cellphone-check', + credit_line: 'bank', + investment: 'chart-line', }; - return icons[type] || '💰'; + return icons[type] || 'wallet'; } diff --git a/task.md b/task.md index a2316d0..119896e 100644 --- a/task.md +++ b/task.md @@ -87,7 +87,7 @@ - [x] 6.4.1 DatePickerModal - 日期选择组件 - [x] 6.4.2 AmountInput - 金额输入组件 - [x] 6.4.3 IconSelector - 图标选择组件 -- [ ] 6.4.4 react-native-vector-icons 集成 +- [x] 6.4.4 react-native-vector-icons 集成 --- @@ -100,13 +100,43 @@ --- -## ⏳ Phase 7: UI 优化 (待开始) +## 🚀 Phase 9: 图表与可视化 (Reports 2.0) +> +> 目标: 集成专业图表库,实现可视化报表 -- [ ] 7.1 主页最近交易列表 -- [ ] 7.2 骨架屏加载效果 -- [ ] 7.3 下拉刷新动画 -- [ ] 7.4 空状态优化 -- [ ] 7.5 错误处理和提示 +- [ ] 9.1 集成图表库 (e.g. react-native-gifted-charts or victory-native) +- [ ] 9.2 收支趋势折线图 (Trend Line Chart) +- [ ] 9.3 支出分类饼图 (Category Pie Chart) +- [ ] 9.4 月度收支对比柱状图 (Bar Chart) + +## 🔄 Phase 10: 核心功能补全 +> +> 目标: 补齐 Web 端定义的 P1 核心记账能力 + +- [ ] 10.1 周期性交易模块 (Recurring Transactions) + - [ ] 10.1.1 周期交易服务 (recurringTransactionService) + - [ ] 10.1.2 周期交易列表页 + - [ ] 10.1.3 创建/编辑周期交易 +- [ ] 10.2 多账本系统 (Ledger System) + - [ ] 10.2.1 账本服务 (ledgerService) + - [ ] 10.2.2 账本管理页 (新建/切换/编辑) + +## 🐷 Phase 11: 财务目标 (Savings & Goals) +> +> 目标: 增强预算模块,增加存钱目标 + +- [ ] 11.1 存钱罐服务 (piggyBankService) +- [ ] 11.2 存钱罐列表组件 (BudgetScreen 集成) +- [ ] 11.3 存入/取出操作逻辑 + +## 🛠️ Phase 12: 工具与生态 +> +> 目标: 完善 P2 功能,对齐 Web 端体验 + +- [ ] 12.1 交易日历视图 (Calendar View) +- [ ] 12.2 数据导出功能 (Export to CSV/Excel) +- [ ] 12.3 汇率换算工具 (Exchange Rate) +- [ ] 12.4 消息通知中心 (Notifications) ---