UI 重设计(P1-P5): - 主题从靛蓝 accent 切换为近黑白单色(#111318),品牌感靠排版与财务语义色 - 暗色模式适配 OLED 纯黑,财务色整体提亮一档保证对比度 - 圆角体系新增 xl(24),卡片/弹窗统一大圆角 - 移除 Caveat/Quicksand 自定义字体,切换为系统字体 - 新增 display(34/800) 字阶用于净资产等大数字展示 - 分类/标签/渠道颜色统一到 palette.ts 色板(12 色循环 + FNV 哈希) 记一笔 NumpadSheet(P3 录入闭环): - 全新计算器风格录入面板:方向 chip → 大金额 → 账户 chip → 分类快捷行 → 数字键盘 - 支持表达式输入(20+15-5.5)与字符串十进制求值,避免浮点精度问题 - 高级模式保留多行分录编辑器,简单模式自动推断双腿 - postingLegs.ts 实现 buildPostings 的逆操作(编辑/草稿预填) - 未保存守卫:整页 beforeRemove + modal 脏状态上报 - 全局弹层(NumpadSheetHost)由+按钮唤起,可通过设置开关 Tab 栏重构: - AppTabBar 替换默认 tab bar,中央+按钮唤起全局记账面板 - Tab 从 5 个精简为 4 个(首页/交易/报表/设置),+按钮独立 管理页模板 ManagementScreen: - 统一「列表 + 新增 + 点按编辑 + 长按删除 + FormModal」模式 - 预算/分类/标签/信用卡等管理页消除手写样板 其他: - OCR 模型从 PP-OCRv5 升级到 v6(det/rec/dict 全部替换) - GBK 解码器 import 修复(text-encoding-gbk ESM 兼容) - 批量确认失败数计算修正(用 failedPayees.length 替代 store 快照差值) - 新增 diagnostics 诊断页、TodoStrip 首页待办条、SwipeableTransactionCard 左滑操作 - periodNav.ts 报表周期导航(anchor+period 统一状态管理,本地时区运算) - 测试覆盖 numpadExpression/postingLegs/periodNav/palette/search/transactionGroups/categoryIcons/dateGrid
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
/**
|
||
* 报表周期导航(P4):单一 anchor 日期 + 周期类型,替代周/月/年三套独立状态。
|
||
* 全部本地时区字符串运算,避免 toISOString 的 UTC 偏移。
|
||
*/
|
||
import { toDateString } from './decimal';
|
||
|
||
export type ReportPeriod = 'weekly' | 'monthly' | 'annual';
|
||
|
||
function parse(dateStr: string): Date {
|
||
const [y, m, d] = dateStr.split('-').map(Number);
|
||
return new Date(y, m - 1, d);
|
||
}
|
||
|
||
/** 平移 anchor:周 ±7 天;月 ±N(月末日钳制);年 ±N。 */
|
||
export function shiftAnchor(anchor: string, period: ReportPeriod, delta: number): string {
|
||
const d = parse(anchor);
|
||
if (period === 'weekly') {
|
||
d.setDate(d.getDate() + 7 * delta);
|
||
} else if (period === 'monthly') {
|
||
const day = d.getDate();
|
||
d.setDate(1);
|
||
d.setMonth(d.getMonth() + delta);
|
||
// 月末钳制:目标月天数不足时退到月末
|
||
const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
||
d.setDate(Math.min(day, lastDay));
|
||
} else {
|
||
d.setFullYear(d.getFullYear() + delta);
|
||
}
|
||
return toDateString(d);
|
||
}
|
||
|
||
/** anchor 所在周期的起止日期(含两端)。周 = 周一~周日。 */
|
||
export function periodRange(anchor: string, period: ReportPeriod): { start: string; end: string } {
|
||
const d = parse(anchor);
|
||
if (period === 'weekly') {
|
||
const day = d.getDay(); // 0=周日
|
||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||
const monday = new Date(d);
|
||
monday.setDate(d.getDate() + diffToMonday);
|
||
const sunday = new Date(monday);
|
||
sunday.setDate(monday.getDate() + 6);
|
||
return { start: toDateString(monday), end: toDateString(sunday) };
|
||
}
|
||
if (period === 'monthly') {
|
||
const start = new Date(d.getFullYear(), d.getMonth(), 1);
|
||
const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
||
return { start: toDateString(start), end: toDateString(end) };
|
||
}
|
||
return { start: `${d.getFullYear()}-01-01`, end: `${d.getFullYear()}-12-31` };
|
||
}
|
||
|
||
/** ISO-8601 周数(周一开头)。 */
|
||
export function isoWeekNumber(dateStr: string): number {
|
||
const date = parse(dateStr);
|
||
date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7));
|
||
const week1 = new Date(date.getFullYear(), 0, 4);
|
||
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7);
|
||
}
|