feat: add account groups, scheduled payments and cash flow calendar

This commit is contained in:
陈煜 committed 2026-10-02 17:42:57 +08:00
1 parent a7e0a0fe58
commit 86c6daf695
46 files changed
+2548 -290

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
import { useId, useRef, useState } from 'react';
import { type Position, money, positionAmount } from './api';
import { t as tr } from './i18n';
import { iconUrl } from './IconLibrary';
export function AccountPicker({
name,
accounts,
value,
onChange,
excludeId,
}: {
name: string;
accounts: Position[];
value: string;
onChange: (id: string) => void;
excludeId?: string;
}) {
const dialog = useRef<HTMLDialogElement>(null),
title = useId();
const [grouped, setGrouped] = useState(true),
[query, setQuery] = useState('');
const selected = accounts.find((p) => p.id === value);
const rows = accounts.filter(
(p) =>
p.id !== excludeId &&
`${p.name} ${p.groupName || ''} ${p.currency}`.toLowerCase().includes(query.toLowerCase()),
);
const groups = grouped ? [...new Set(rows.map((p) => p.groupName || '未分组'))] : ['全部账户'];
return (
<div className="account-picker">
<input name={name} type="hidden" value={value} />
<button
className="secondary picker-trigger"
type="button"
onClick={() => dialog.current?.showModal()}
>
{selected
? `${selected.name} · ${money(positionAmount(selected), selected.currency)}`
: tr('选择账户')}
</button>
<dialog ref={dialog} className="picker-dialog" aria-labelledby={title}>
<div className="panel-title">
<h2 id={title}>{tr('选择账户')}</h2>
<button type="button" onClick={() => dialog.current?.close()} aria-label={tr('关闭')}>
×
</button>
</div>
<input
placeholder={tr('搜索账户、分组或币种')}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className="actions">
<button type="button" aria-pressed={!grouped} onClick={() => setGrouped(false)}>
{tr('全部账户')}
</button>
<button type="button" aria-pressed={grouped} onClick={() => setGrouped(true)}>
{tr('按分组显示')}
</button>
</div>
{groups.map((group) => (
<section key={group}>
<h3>{tr(group)}</h3>
{rows
.filter((p) => !grouped || (p.groupName || '未分组') === group)
.map((p) => (
<button
type="button"
className="picker-option"
key={p.id}
onClick={() => {
onChange(p.id);
dialog.current?.close();
}}
>
{p.iconId && <img src={iconUrl(p.iconId)} alt="" />}
<span>
{p.name}
<small>{money(positionAmount(p), p.currency)}</small>
</span>
{p.id === value && <span>✓</span>}
</button>
))}
</section>
))}
{!rows.length && <p>{tr('没有符合条件的账户')}</p>}
</dialog>
</div>
);
}