feat: 实现交易和账户列表及详情页,并新增通用选择器组件和主标签导航。

This commit is contained in:
2026-01-28 01:03:47 +08:00
parent 5555f7d795
commit 72571b2b9d
15 changed files with 519 additions and 128 deletions

View File

@@ -117,3 +117,5 @@ dependencies {
implementation jscFlavor
}
}
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

278
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<string, string> = {
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}
>
<View style={styles.accountIcon}>
<Text style={styles.accountIconText}>{getAccountIcon(item)}</Text>
<AppIcon
name={getAccountIcon(item)}
size={22}
color={colors.primary[500]}
/>
</View>
<View style={styles.accountInfo}>
<Text style={styles.accountName}>{item.name}</Text>
@@ -195,7 +200,12 @@ export function AccountSelector({
>
{selectedAccount ? (
<View style={styles.selectedContent}>
<Text style={styles.selectedIcon}>{getAccountIcon(selectedAccount)}</Text>
<AppIcon
name={getAccountIcon(selectedAccount)}
size={24}
color={colors.primary[500]}
style={{ marginRight: spacing.md }}
/>
<View style={styles.selectedInfo}>
<Text style={styles.selectedText}>{selectedAccount.name}</Text>
<Text style={styles.selectedBalance}>{formatBalance(selectedAccount.balance)}</Text>

View File

@@ -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<AppIconProps> = ({ name, size = 24, color, style }) => {
// Check if likely emoji (non-ascii)
const isEmoji = /[^\u0000-\u007F]/.test(name);
if (isEmoji) {
return <Text style={[{ fontSize: size, color }, style]}>{name}</Text>;
}
return <MaterialCommunityIcons name={name} size={size} color={color} style={style} />;
};

View File

@@ -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<string, string> = {
// 支出分类
: '🍔',
: '🚗',
: '🛒',
: '🎮',
: '💊',
: '📚',
: '🏠',
: '📱',
: '👔',
: '💄',
: '',
: '✈️',
: '🐱',
: '👥',
: '🎁',
: '💼',
: '💻',
: '📦',
: '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,
]}
>
<Text style={styles.categoryIconText}>{getCategoryIcon(item)}</Text>
<AppIcon
name={getCategoryIcon(item)}
size={28}
color={currentId === item.id ? '#FFFFFF' : colors.text}
/>
</View>
<Text
style={[
@@ -213,7 +218,12 @@ export function CategorySelector({
>
{selectedCategory ? (
<View style={styles.selectedContent}>
<Text style={styles.selectedIcon}>{getCategoryIcon(selectedCategory)}</Text>
<AppIcon
name={getCategoryIcon(selectedCategory)}
size={20}
color={colors.text}
style={{ marginRight: spacing.sm }}
/>
<Text style={styles.selectedText}>{selectedCategory.name}</Text>
</View>
) : (

View File

@@ -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)}
>
<Text style={styles.iconEmoji}>{item}</Text>
<AppIcon
name={item}
size={28}
color={selectedIcon === item ? colors.primary[500] : colors.text}
/>
</TouchableOpacity>
)}
/>
@@ -226,7 +231,4 @@ const createStyles = (colors: any, isDark: boolean) =>
borderWidth: 2,
borderColor: colors.primary[500],
},
iconEmoji: {
fontSize: 28,
},
});

View File

@@ -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';

View File

@@ -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<MainTabParamList>();
// 简单的图标组件
function TabIcon({ name, focused }: { name: string; focused: boolean }) {
const iconMap: Record<string, string> = {
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 (
<Text style={{ fontSize: focused ? 26 : 24, opacity: focused ? 1 : 0.7 }}>
{iconMap[name] || '📱'}
</Text>
<AppIcon
name={iconMap[name] || 'circle'}
size={24}
color={color} // Logic in Navigator handles color usually, but we pass it anyway or let parent handle style
/>
);
}
@@ -50,7 +55,7 @@ function AddButton() {
style={[styles.addButton, { backgroundColor: colors.primary[500] }]}
activeOpacity={0.8}
>
<Text style={styles.addButtonText}>+</Text>
<AppIcon name="plus" size={32} color="#FFFFFF" />
</TouchableOpacity>
);
}
@@ -76,8 +81,18 @@ export default function MainTabNavigator() {
},
tabBarActiveTintColor: colors.primary[500],
tabBarInactiveTintColor: colors.textSecondary,
tabBarIcon: ({ focused }) => (
<TabIcon name={route.name} focused={focused} />
tabBarIcon: ({ focused, color }) => (
<AppIcon
name={
route.name === 'Home' ? (focused ? 'home' : 'home-outline') :
route.name === 'Transactions' ? (focused ? 'file-document' : 'file-document-outline') :
route.name === 'Accounts' ? (focused ? 'wallet' : 'wallet-outline') :
route.name === 'Settings' ? (focused ? 'cog' : 'cog-outline') :
'circle'
}
size={24}
color={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,
},
});

View File

@@ -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<RootStackParamList, 'AccountDetail'>;
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
@@ -118,7 +119,11 @@ export default function AccountDetailScreen() {
onPress={() => navigation.navigate('TransactionDetail', { transactionId: item.id })}
>
<View style={styles.transactionIcon}>
<Text style={styles.transactionEmoji}>{item.categoryIcon || '📦'}</Text>
<AppIcon
name={item.categoryIcon || 'package-variant'}
size={20}
color={colors.primary[500]}
/>
</View>
<View style={styles.transactionInfo}>
<Text style={styles.transactionCategory}>{item.categoryName}</Text>
@@ -158,7 +163,7 @@ export default function AccountDetailScreen() {
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Text style={styles.backIcon}></Text>
<AppIcon name="arrow-left" size={24} color={colors.text} />
</TouchableOpacity>
<Text style={styles.headerTitle}></Text>
<TouchableOpacity style={styles.headerButton} onPress={handleEdit}>
@@ -180,7 +185,11 @@ export default function AccountDetailScreen() {
{/* 账户卡片 */}
<View style={styles.accountCard}>
<View style={styles.accountIconContainer}>
<Text style={styles.accountIcon}>{account.icon || '💳'}</Text>
<AppIcon
name={account.icon || 'credit-card'}
size={36}
color="#FFFFFF"
/>
</View>
<Text style={styles.accountName}>{account.name}</Text>
<Text style={styles.accountType}>{getAccountTypeLabel(account.type)}</Text>
@@ -229,7 +238,7 @@ export default function AccountDetailScreen() {
/>
) : (
<View style={styles.emptyTransactions}>
<Text style={styles.emptyIcon}>📝</Text>
<AppIcon name="clipboard-text-outline" size={40} color={colors.textTertiary} style={{ marginBottom: spacing.md }} />
<Text style={styles.emptyTransactionsText}></Text>
</View>
)}

View File

@@ -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<RootStackParamList>;
@@ -79,14 +80,14 @@ export default function AccountsScreen() {
const getAccountIcon = (type: string) => {
const icons: Record<string, string> = {
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')}
>
<Text style={styles.transferButtonText}>🔄 </Text>
<AppIcon name="swap-horizontal" size={16} color={colors.semantic.transfer} style={{ marginRight: 4 }} />
<Text style={styles.transferButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.addButton}
onPress={() => navigation.navigate('AddAccount')}
>
<Text style={styles.addButtonText}>+ </Text>
<AppIcon name="plus" size={16} color="#FFFFFF" style={{ marginRight: 4 }} />
<Text style={styles.addButtonText}></Text>
</TouchableOpacity>
</View>
</View>
@@ -163,7 +166,11 @@ export default function AccountsScreen() {
onPress={() => navigation.navigate('AccountDetail', { accountId: account.id })}
>
<View style={styles.accountIcon}>
<Text style={styles.accountIconText}>{getAccountIcon(account.type)}</Text>
<AppIcon
name={getAccountIcon(account.type)}
size={22}
color={colors.primary[500]}
/>
</View>
<View style={styles.accountInfo}>
<Text style={styles.accountName}>{account.name}</Text>
@@ -191,7 +198,11 @@ export default function AccountsScreen() {
onPress={() => navigation.navigate('AccountDetail', { accountId: account.id })}
>
<View style={styles.accountIcon}>
<Text style={styles.accountIconText}>{getAccountIcon(account.type)}</Text>
<AppIcon
name={getAccountIcon(account.type)}
size={22}
color={colors.primary[500]}
/>
</View>
<View style={styles.accountInfo}>
<Text style={styles.accountName}>{account.name}</Text>
@@ -209,7 +220,7 @@ export default function AccountsScreen() {
{/* 空状态 */}
{accounts.length === 0 && (
<View style={styles.emptyState}>
<Text style={styles.emptyIcon}>💳</Text>
<AppIcon name="credit-card-off-outline" size={48} color={colors.textTertiary} style={{ marginBottom: 16 }} />
<Text style={styles.emptyText}></Text>
<Text style={styles.emptySubText}></Text>
</View>
@@ -219,6 +230,7 @@ export default function AccountsScreen() {
);
}
const createStyles = (colors: any, isDark: boolean, insets: any) =>
StyleSheet.create({
container: {

View File

@@ -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<RootStackParamList, 'TransactionDetail'>;
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
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()}
>
<Text style={styles.backIcon}></Text>
<AppIcon name="arrow-left" size={24} color={colors.text} />
</TouchableOpacity>
<Text style={styles.headerTitle}></Text>
<View style={styles.headerActions}>
@@ -131,7 +132,7 @@ export default function TransactionDetailScreen() {
{/* 金额卡片 */}
<View style={styles.amountCard}>
<View style={[styles.typeIcon, { backgroundColor: amountColor + '20' }]}>
<Text style={styles.typeEmoji}>{typeConfig.icon}</Text>
<AppIcon name={typeConfig.icon} size={32} color={amountColor} />
</View>
<Text style={[styles.amount, { color: amountColor }]}>
{transaction.type === 'income' ? '+' : '-'}
@@ -146,9 +147,12 @@ export default function TransactionDetailScreen() {
label="分类"
value={
<View style={styles.categoryValue}>
<Text style={styles.categoryIcon}>
{transaction.categoryIcon || '📦'}
</Text>
<AppIcon
name={transaction.categoryIcon || 'package-variant'}
size={20}
color={colors.text}
style={{ marginRight: 8 }}
/>
<Text style={styles.categoryName}>
{transaction.categoryName}
</Text>
@@ -161,9 +165,12 @@ export default function TransactionDetailScreen() {
label="账户"
value={
<View style={styles.categoryValue}>
<Text style={styles.categoryIcon}>
{transaction.accountIcon || '💳'}
</Text>
<AppIcon
name={transaction.accountIcon || 'credit-card'}
size={20}
color={colors.text}
style={{ marginRight: 8 }}
/>
<Text style={styles.categoryName}>
{transaction.accountName}
</Text>
@@ -177,9 +184,12 @@ export default function TransactionDetailScreen() {
label="转入账户"
value={
<View style={styles.categoryValue}>
<Text style={styles.categoryIcon}>
{transaction.toAccountIcon || '💳'}
</Text>
<AppIcon
name={transaction.toAccountIcon || 'credit-card'}
size={20}
color={colors.text}
style={{ marginRight: 8 }}
/>
<Text style={styles.categoryName}>
{transaction.toAccountName}
</Text>

View File

@@ -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<RootStackParamList>;
@@ -105,9 +106,11 @@ export default function TransactionsScreen() {
onPress={() => navigation.navigate('TransactionDetail', { transactionId: item.id })}
>
<View style={[styles.iconContainer, { backgroundColor: getTypeColor(item.type) + '20' }]}>
<Text style={styles.iconText}>
{item.categoryIcon || (item.type === 'income' ? '💰' : item.type === 'expense' ? '💸' : '🔄')}
</Text>
<AppIcon
name={item.categoryIcon || (item.type === 'income' ? 'cash-plus' : item.type === 'expense' ? 'cash-minus' : 'swap-horizontal')}
size={22}
color={getTypeColor(item.type)}
/>
</View>
<View style={styles.transactionInfo}>
<Text style={styles.transactionNote} numberOfLines={1}>
@@ -155,7 +158,7 @@ export default function TransactionsScreen() {
onEndReachedThreshold={0.3}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text style={styles.emptyIcon}>📝</Text>
<AppIcon name="clipboard-text-outline" size={48} color={colors.textTertiary} style={{ marginBottom: 16 }} />
<Text style={styles.emptyText}></Text>
</View>
}

View File

@@ -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<string, string> = {
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';
}

44
task.md
View File

@@ -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)
---