add .gitignore and App.tsx

This commit is contained in:
fengmengqi 2026-07-13 13:35:27 +08:00
parent 2086f67e68
commit 167adfca62
2 changed files with 47 additions and 0 deletions

1
.gitignore vendored
View File

@ -39,3 +39,4 @@ outputs/
# MiMoCode # MiMoCode
.mimocode/ .mimocode/
.agent/

46
App.tsx Normal file
View File

@ -0,0 +1,46 @@
import { useMemo, useState } from 'react';
import { Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { classify, commitMobileTransaction, importStatement, parseLedger, type ImportedEvent, type Rule, validateTransaction } from './src/domain';
const sampleLedger = `option "operating_currency" "CNY"
2026-01-01 open Assets:Alipay CNY
2026-01-01 open Assets:WeChat CNY
2026-01-01 open Expenses:Food CNY
2026-01-01 open Expenses:Uncategorized CNY
2026-01-01 open Income:Uncategorized CNY
include "mobile.bean"
`;
const sampleStatement = '交易时间,金额(元),收/支,交易对方,商品说明,交易订单号\n2026-07-01,24.50,支出,咖啡店,冰美式,ORDER-1\n2026-07-02,5000,收入,公司,工资,ORDER-2';
const rules: Rule[] = [{ id: 'coffee', priority: 100, channel: 'Alipay', counterpartyContains: '咖啡', channelAccount: 'Assets:Alipay', categoryAccount: 'Expenses:Food', narration: '咖啡', tags: ['food'], hits: 0 }];
type Tab = '账本' | '记账' | '导入' | '规则' | '诊断';
export default function App() {
const ledger = useMemo(() => parseLedger([{ path: 'main.bean', content: sampleLedger }]), []);
const [tab, setTab] = useState<Tab>('账本'); const [mobileBean, setMobileBean] = useState(''); const [events, setEvents] = useState<ImportedEvent[]>([]);
const [message, setMessage] = useState('账本已解析。手机交易只会写入 mobile.bean。');
const [amount, setAmount] = useState(''); const [account, setAccount] = useState('Expenses:Food');
const addManual = () => {
const draft = { date: '2026-07-10', narration: '手工记账', postings: [{ account: 'Assets:Alipay', amount: `-${amount}`, currency: 'CNY' }, { account, amount, currency: 'CNY' }] };
const result = validateTransaction(draft, ledger); if (!result.valid) return setMessage(result.errors.join(''));
try { const commit = commitMobileTransaction(draft, ledger, mobileBean); setMobileBean(commit.content); setAmount(''); setMessage('已追加到 mobile.bean。'); } catch (error) { setMessage(String(error)); }
};
const loadDemo = () => { setEvents(importStatement(sampleStatement, 'alipay-csv-v1')); setMessage('已导入 2 条账单;请逐条确认。'); };
const confirm = (event: ImportedEvent) => {
const candidate = classify(event, rules, ledger); try { const commit = commitMobileTransaction(candidate.draft, ledger, mobileBean); setMobileBean(commit.content); setEvents(current => current.filter(item => item.id !== event.id)); setMessage(candidate.ruleId ? '已按规则确认并写入 mobile.bean。' : '未匹配规则,已确认未分类分录。'); } catch (error) { setMessage(String(error)); }
};
return <SafeAreaView style={styles.page}><StatusBar style="dark" />
<View style={styles.header}><Text style={styles.title}>Bean Mobile</Text><Text style={styles.subtitle}>{message}</Text></View>
<View style={styles.tabs}>{(['账本', '记账', '导入', '规则', '诊断'] as Tab[]).map(item => <Pressable key={item} onPress={() => setTab(item)} style={[styles.tab, tab === item && styles.activeTab]}><Text style={tab === item ? styles.activeText : styles.tabText}>{item}</Text></Pressable>)}</View>
<ScrollView contentContainerStyle={styles.content}>
{tab === '账本' && <><Card title="账本状态"><Text>{ledger.accounts.size} · {ledger.transactions.length} </Text><Text style={styles.muted}> include "mobile.bean"</Text></Card><Card title="mobile.bean"><Text>{mobileBean || '尚无手机端交易'}</Text></Card></>}
{tab === '记账' && <Card title="手工复式记账"><TextInput value={amount} onChangeText={setAmount} keyboardType="decimal-pad" placeholder="金额,例如 24.50" style={styles.input}/><TextInput value={account} onChangeText={setAccount} placeholder="费用账户" style={styles.input}/><Button label="校验并追加到 mobile.bean" onPress={addManual}/></Card>}
{tab === '导入' && <><Card title="账单自动记账"><Text style={styles.muted}> CSV</Text><Button label="导入演示支付宝 CSV" onPress={loadDemo}/></Card>{events.map(event => { const candidate = classify(event, rules, ledger); return <Card key={event.id} title={`${event.occurredAt} · ${event.amount} ${event.currency}`}><Text>{event.counterparty} · {event.memo}</Text><Text style={styles.muted}>{candidate.ruleId ? `规则:${candidate.ruleId}` : '未匹配:将使用 Uncategorized'}</Text><Text>{candidate.draft.postings.map(posting => `${posting.account} ${posting.amount} ${posting.currency}`).join('\n')}</Text><Button label="确认入账" onPress={() => confirm(event)}/></Card>; })}</>}
{tab === '规则' && <Card title="自动分类规则">{rules.map(rule => <View key={rule.id} style={styles.row}><Text>{rule.id}</Text><Text style={styles.muted}>{rule.counterpartyContains} {rule.categoryAccount}</Text></View>)}</Card>}
{tab === '诊断' && <Card title="只读兼容与诊断"><Text>{ledger.diagnostics.length}</Text><Text>{ledger.unsupported.length}</Text><Text style={styles.muted}></Text></Card>}
</ScrollView>
</SafeAreaView>;
}
function Card({ title, children }: { title: string; children: React.ReactNode }) { return <View style={styles.card}><Text style={styles.cardTitle}>{title}</Text>{children}</View>; }
function Button({ label, onPress }: { label: string; onPress: () => void }) { return <Pressable accessibilityRole="button" onPress={onPress} style={styles.button}><Text style={styles.buttonText}>{label}</Text></Pressable>; }
const styles = StyleSheet.create({ page: { flex: 1, backgroundColor: '#f6f7f9' }, header: { padding: 20, paddingBottom: 12 }, title: { fontSize: 28, fontWeight: '700' }, subtitle: { color: '#57606a', marginTop: 6 }, tabs: { flexDirection: 'row', backgroundColor: '#fff', borderBottomWidth: 1, borderColor: '#e5e7eb' }, tab: { flex: 1, paddingVertical: 12, alignItems: 'center' }, activeTab: { borderBottomWidth: 2, borderColor: '#146c43' }, tabText: { color: '#57606a' }, activeText: { color: '#146c43', fontWeight: '700' }, content: { padding: 16, gap: 12 }, card: { backgroundColor: '#fff', borderRadius: 12, padding: 16, gap: 8, shadowColor: '#000', shadowOpacity: .04, shadowRadius: 6 }, cardTitle: { fontSize: 18, fontWeight: '700' }, muted: { color: '#687078', lineHeight: 20 }, input: { borderWidth: 1, borderColor: '#d0d7de', borderRadius: 8, padding: 11, backgroundColor: '#fff' }, button: { marginTop: 6, backgroundColor: '#146c43', padding: 12, borderRadius: 8, alignItems: 'center' }, buttonText: { color: '#fff', fontWeight: '700' }, row: { borderTopWidth: 1, borderColor: '#eef0f2', paddingTop: 10, gap: 3 } });