add .gitignore and App.tsx
This commit is contained in:
parent
2086f67e68
commit
167adfca62
1
.gitignore
vendored
1
.gitignore
vendored
@ -39,3 +39,4 @@ outputs/
|
|||||||
|
|
||||||
# MiMoCode
|
# MiMoCode
|
||||||
.mimocode/
|
.mimocode/
|
||||||
|
.agent/
|
||||||
|
|||||||
46
App.tsx
Normal file
46
App.tsx
Normal 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 } });
|
||||||
Loading…
Reference in New Issue
Block a user