/** * 分类管理页面(plan.md「1.1 category/index」+ 决策 1 双轨制)。 * * 功能:分类列表(支出/收入 chips 切换)+ 添加/编辑/删除。 * 数据来源:metadataStore(持久化),linkedAccount 映射到 Beancount 账户。 * P5:套 ManagementScreen 模板,headerContent 放支出/收入 chips。 */ import React, { useMemo, useState } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { useTheme, createCommonStyles } from '../../theme'; import { useMetadataStore, generateId } from '../../store/metadataStore'; import { useT } from '../../i18n'; import { Card } from '../../components/Card'; import { ManagementScreen } from '../../components/ManagementScreen'; import type { Category } from '../../domain/categories'; export default function CategoryScreen() { const { theme } = useTheme(); const t = useT(); const commonStyles = useMemo(() => createCommonStyles(theme), [theme]); const categories = useMetadataStore(s => s.categories); const addCategory = useMetadataStore(s => s.addCategory); const updateCategory = useMetadataStore(s => s.updateCategory); const removeCategory = useMetadataStore(s => s.removeCategory); /** 当前展示的分类类型(支出/收入),新增分支按此落库。 */ const [catType, setCatType] = useState<'expense' | 'income'>('expense'); return ( title={t('category.title')} items={categories.filter(c => c.type === catType)} keyExtractor={cat => cat.id} addLabel={t('category.addTitle', { type: catType === 'income' ? t('category.income') : t('category.expense') })} headerContent={ {(['expense', 'income'] as const).map(tp => ( setCatType(tp)} style={[commonStyles.chip, catType === tp && commonStyles.chipActive]} > {tp === 'expense' ? t('category.expense') : t('category.income')} ))} } renderItem={(cat, { openEdit, confirmDelete }) => ( [{ opacity: pressed ? 0.6 : 1 }]} > {cat.linkedAccount} {cat.keywords.length > 0 && ( {t('category.keywordsLabel')}: {cat.keywords.join(', ')} )} )} formTitle={editing => (editing ? t('category.editTitle') : t('category.addTitle', { type: catType === 'income' ? t('category.income') : t('category.expense') }))} formFields={editing => [ { key: 'name', label: t('category.fieldName'), placeholder: t('category.namePlaceholder'), defaultValue: editing?.name }, { key: 'linkedAccount', label: t('category.fieldLinkedAccount'), placeholder: t('category.accountPlaceholder'), defaultValue: editing?.linkedAccount }, { key: 'keywords', label: t('category.fieldKeywords'), placeholder: t('category.keywordsPlaceholder'), defaultValue: editing?.keywords.join(', ') }, ]} onSubmit={(values, editing) => { if (editing) { updateCategory(editing.id, { name: values.name || editing.name, linkedAccount: values.linkedAccount || editing.linkedAccount, keywords: values.keywords ? values.keywords.split(/[,,\s]+/).filter(Boolean) : [], }); } else { addCategory({ id: generateId('cat'), name: values.name || t('common.untitled'), type: catType, linkedAccount: values.linkedAccount || (catType === 'income' ? 'Income:Uncategorized' : 'Expenses:Uncategorized'), keywords: values.keywords ? values.keywords.split(/[,,\s]+/).filter(Boolean) : [], }); } return true; }} onDelete={cat => removeCategory(cat.id)} deleteConfirmText={cat => t('category.deleteConfirm', { name: cat.name })} deleteConfirmTitle={t('category.deleteTitle')} footer={ {t('common.clickEditLongDelete')} } /> ); } const styles = StyleSheet.create({ chipRow: { flexDirection: 'row', gap: 8 }, });