dsh-personal-ledger 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +13 -6
- package/client.js +135 -15
- package/package.json +7 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 更新日志(Changelog)
|
|
2
|
+
|
|
3
|
+
本文件的版本号与 `package.json.version`、`README.md` 顶部版本号保持一致。
|
|
4
|
+
|
|
5
|
+
## 1.4.0
|
|
6
|
+
|
|
7
|
+
新增功能:金额输入支持算式自动计算(编辑账单时调整总额)。
|
|
8
|
+
|
|
9
|
+
- 「总金额」输入框现在可以直接写算式:`3994.28+100.00`(→ ¥4,094.28)、`3994.28-100.00`、`1000+200-50`
|
|
10
|
+
等多段加减;支持全角 `+`/`-`/`−`、千分位与空格。
|
|
11
|
+
- 只写调整量也支持:`+300` / `-200` 表示在**当前总额**上加减(例:当前 ¥1,000.00 输入 `+300` 保存为 ¥1,300.00)。
|
|
12
|
+
- 编辑对话框实时预览:算式显示 `计算结果:{formula} = {after}`,相对调整显示
|
|
13
|
+
`调整后总额:{before} {sign} {delta} = {after}`,每期金额随之重算;保存时按「期号顺序」保留已还状态。
|
|
14
|
+
- 无效输入有明确提示且不会写入数据:结果 ≤ 0 或超上限(`errAdjustRange`)、算式格式错误(`errAdjustInvalid`)、
|
|
15
|
+
低于「每期 0.01 元」(沿用 `errAmountSmall`)。
|
|
16
|
+
- 新增账单同样支持算式输入;不带算式的 `+500` / `-500` 在新增时分别等价于 500 / 无效。
|
|
17
|
+
- 测试:`test/` 的 `parseAmountInput` 套件覆盖算式、相对调整、非法算式与边界;`e2e/` 第 9 步新增
|
|
18
|
+
算式流程(`900+50` → 950.00、`950-100.50` → 849.50、`100-200` 拒绝且不落库)。
|
|
19
|
+
|
|
20
|
+
## 1.3.2
|
|
21
|
+
|
|
22
|
+
稳定性修复(导入 / 导出往返):
|
|
23
|
+
|
|
24
|
+
- 修复英文界面导出的 CSV 回灌时多出一条「第 N 行无法识别」误报:导入解析现在会识别英文月度汇总表头
|
|
25
|
+
`Month / Due (CNY) / Paid (CNY) / Unpaid (CNY)` 并在解析账单前停止(原先只识别中文「还款月份」)。
|
|
26
|
+
- 修复名称列为空的 CSV 行会生成无名账单、随后刷新页面被静默丢弃的问题:这类行现在回填当前平台默认名
|
|
27
|
+
(如「通用 / CSV」);没有可用默认名时按「第 N 行无法识别」提示,而不是悄悄丢数据。
|
|
28
|
+
- 新增离线测试套件 `test/`(124 个用例)与真实浏览器端到端测试 `e2e/`(headless Chrome + CDP,8 个流程),
|
|
29
|
+
覆盖添加 / 编辑 / 按月 / 一键还清 / 导入 / 导出 / 刷新持久化等核心路径。
|
|
30
|
+
|
|
31
|
+
## 1.3.1
|
|
32
|
+
|
|
33
|
+
导出账单 CSV 可直接回灌导入(round-trip):
|
|
34
|
+
|
|
35
|
+
- 导出 CSV 增加 `已还期数` 列;导入时还原为对应期数的已还状态(已还时间记为导入时刻)。
|
|
36
|
+
- 导入支持插件导出 CSV 的表头:`类型 / 名称 / 总额(元) / 期数 / 已还期数 / 剩余(元) / 首期/到期日 / 下次到期日 / 来源 / 备注`。
|
|
37
|
+
- `来源` 列的中英文平台名映射回平台标记;文件末尾的「还款月份 / 应还 / 已还 / 待还」汇总表被自动跳过。
|
|
38
|
+
- 缺少可识别表头的多列表格提示「缺少可识别表头」,不再猜测金额生成错误账单。
|
|
39
|
+
- 名称按字面值保留(含逗号的名称按 CSV 引号规则往返)。
|
|
40
|
+
|
|
41
|
+
## 1.3.0
|
|
42
|
+
|
|
43
|
+
- 编辑账单:可按类型 / 名称 / 总额 / 期数 / 首期还款日 / 备注编辑,按期号顺序保留已还状态。
|
|
44
|
+
- 按月份视图与一键还清本月:按到期月份分组统计应还 / 已还 / 待还与逾期笔数,可整月标记已还。
|
|
45
|
+
- 导出:文本(.txt)/ 表格(.csv,UTF-8 BOM)/ 图片(.png,按主题绘制)。
|
|
46
|
+
- 账单导入:京东白条 / 信用卡 / 支付宝备用金 / 支付宝花呗 / 通用 CSV 五种平台预设。
|
|
47
|
+
- 新增「不分期(一次性)」账单类型,与分期账单共用还款计划结构(1 期)。
|
|
48
|
+
|
|
49
|
+
## 1.2.x 及更早
|
|
50
|
+
|
|
51
|
+
分期账单记录、逐期标记已还、汇总与筛选、本地 localStorage 持久化、删除确认、中英文跟随 Harness 语言。
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 分期记账(Installment Ledger)
|
|
2
2
|
|
|
3
|
-
> npm 包名:`dsh-personal-ledger` · 版本 `1.
|
|
3
|
+
> npm 包名:`dsh-personal-ledger` · 版本 `1.4.0`
|
|
4
4
|
|
|
5
5
|
DeepSeek Harness Web UI 插件:在左侧栏提供「分期记账」入口,打开一个记账面板,用来记录 / 编辑**分期账单**和**不分期(一次性)账单**,从**京东白条 / 信用卡 / 支付宝备用金 / 支付宝花呗**等平台**导入账单**,按**还款月份**统计应还金额并**一键还清当月**,还可将账单**导出为文本 / 表格(CSV)/ 图片(PNG)**。
|
|
6
6
|
|
|
@@ -10,6 +10,11 @@ DeepSeek Harness Web UI 插件:在左侧栏提供「分期记账」入口,
|
|
|
10
10
|
- 分期账单:名称、总金额、期数(3/6/12/24 快捷或 1–120)、首期还款日期、备注;自动生成每期计划。
|
|
11
11
|
- 不分期账单:名称、总金额、还款日期、备注;只有一笔到期记录。
|
|
12
12
|
- **编辑账单(本次新增)**:每张卡片都有「编辑」按钮,可修改类型、名称、总额、期数、首期还款日期与备注。保存时按「期号顺序」保留已还状态:仍存在的期保留原已还时间;改期数 / 改类型时,能对应上的期保留已还,新增的期从未还开始。
|
|
13
|
+
- **金额自动计算(本次新增)**:编辑账单时,「总金额」输入框支持**算式**与**相对加减**,保存时自动计算并重算总额与每期金额:
|
|
14
|
+
- 直接输入算式:`3994.28+100.00` → 保存为 ¥4,094.28;`3994.28-100.00`、`1000+200-50` 等多段加减也可;
|
|
15
|
+
- 或只写调整量:`+300` / `-200`(全角 +/-/− 也可),表示在**当前总额**上加减,例如当前 ¥1,000.00 输入 `+300` 保存为 ¥1,300.00;
|
|
16
|
+
- 支持千分位与空格(`3,994.28 + 100`);预览区实时显示 `计算结果:900+50 = ¥950.00` 或 `调整后总额:¥1,000.00 + ¥300.00 = ¥1,300.00`,每期金额同步变化;
|
|
17
|
+
- 已还状态按期号保留;结果 ≤ 0、超过上限、低于「每期 0.01 元」或算式格式错误时会提示且不保存。新增账单同样支持算式输入;`+500`/`-500` 在新增时分别等价于 500 / 无效。
|
|
13
18
|
- **按月份分类与一键还款(本次新增)**:工具栏可切换「账单列表 / 按月份」。
|
|
14
19
|
- 按到期月份分组,显示每月应还 / 已还 / 待还金额与逾期笔数;当前月份高亮。
|
|
15
20
|
- 每个月份卡片可展开逐笔查看(账单名、第几期、到期日、金额、状态),也可单独「标记已还」。
|
|
@@ -51,10 +56,10 @@ DeepSeek Harness Web UI 插件:在左侧栏提供「分期记账」入口,
|
|
|
51
56
|
类型,名称,总额(元),期数,已还期数,剩余(元),首期/到期日,下次到期日,来源,备注
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
- 名称按原样保留(带英文逗号的名称会自动按 CSV
|
|
59
|
+
- 名称按原样保留(带英文逗号的名称会自动按 CSV 引号规则处理);名称列为空白的行会用当前平台默认名回填,不会生成无名账单;
|
|
55
60
|
- **已还期数**会还原为对应期数的已还状态(已还时间记为导入时刻);「剩余(元)」「下次到期日」是导出时的派生列,导入时按总额与期数重新计算,不参与导入;
|
|
56
61
|
- **来源**列的中英文平台名会映射回平台标记(空值表示无来源);
|
|
57
|
-
- 文件末尾的「还款月份 / 应还 / 已还 /
|
|
62
|
+
- 文件末尾的「还款月份 / 应还 / 已还 / 待还」汇总表会被自动忽略,不会被当成账单导入;英文界面导出的 `Month / Due (CNY) / Paid (CNY) / Unpaid (CNY)` 汇总表同样会被忽略;
|
|
58
63
|
- 缺少可识别表头的多列表格会提示「缺少可识别表头」而不会猜测金额、生成错误账单。
|
|
59
64
|
|
|
60
65
|
## 导出格式说明
|
|
@@ -129,11 +134,13 @@ DeepSeek Harness Web UI 插件:在左侧栏提供「分期记账」入口,
|
|
|
129
134
|
|
|
130
135
|
## 安装与验证
|
|
131
136
|
|
|
132
|
-
- 从 npm 安装:在 Harness 左侧栏「插件 / Plugins」页点「Add plugin」,输入包名 `dsh-personal-ledger`(可带版本号,如 `dsh-personal-ledger@1.
|
|
137
|
+
- 从 npm 安装:在 Harness 左侧栏「插件 / Plugins」页点「Add plugin」,输入包名 `dsh-personal-ledger`(可带版本号,如 `dsh-personal-ledger@1.4.0`)。
|
|
133
138
|
|
|
134
139
|
- `node --check index.js`、`node --check client.js` 通过;`package.json`、`locale/*.json` 为合法 JSON;`icon.svg` 为合法 SVG。
|
|
135
140
|
- 安装后经 `cordis_inspect_query` 的 Client `Slots.listSubTree` 确认:`sidebar.panellist` 出现 `installment-ledger` 占用项,`main` 出现 `installment-ledger` 键。
|
|
136
|
-
-
|
|
141
|
+
- 离线单元 + 渲染烟雾测试:`node test/run-tests.mjs`(124 个用例,覆盖金额 / 日期 / 分期拆分、类型切换、旧数据兼容、五种导入解析、账单编辑(已还状态保留)、按月分组与一键还款、文本 / CSV / 图片导出、导出 CSV 回灌、词典与占位符一致性、槽位注册与组件渲染)。
|
|
142
|
+
- 真实浏览器端到端测试:`node e2e/run-e2e.mjs`(headless Chrome + CDP 驱动真实 Web UI:打开面板、添加账单、标记已还 + 刷新持久化、按月份 + 一键还清、CSV 导入 / 去重、CSV / TXT 导出下载、重置空态)。
|
|
143
|
+
- 开发方式与本地 link 安装流程见同目录 `DEV.md`。
|
|
137
144
|
|
|
138
145
|
## 已知限制
|
|
139
146
|
|
|
@@ -142,4 +149,4 @@ DeepSeek Harness Web UI 插件:在左侧栏提供「分期记账」入口,
|
|
|
142
149
|
- 导入解析基于常见文案 / 表头的启发式规则:不同银行或平台的导出格式可能不同,请先在「识别结果」里核对候选账单再导入。
|
|
143
150
|
- 数据存于当前浏览器,不跨浏览器 / 设备同步,也不写入 Session 日志或 Host 存储;清除站点数据会清空账本。
|
|
144
151
|
- 本插件未注册模型可调用的工具,模型无法直接读写账本。
|
|
145
|
-
-
|
|
152
|
+
- 当前安装环境没有交互式浏览器控制,核心交互已由 headless Chrome 端到端测试覆盖;像素级外观仍建议在 Web UI 中人工确认。
|
package/client.js
CHANGED
|
@@ -111,6 +111,11 @@ window.__ModuleLoader__.load({
|
|
|
111
111
|
errName: 'Enter a bill name.',
|
|
112
112
|
errAmount: 'Enter an amount greater than 0 with at most 2 decimals.',
|
|
113
113
|
errAmountSmall: 'The total is too small: every period needs at least ¥0.01.',
|
|
114
|
+
amountAdjustHint: 'Tip: type a formula like 3994.28+100.00, or +100 / -200 to adjust the current total.',
|
|
115
|
+
previewAdjust: 'Adjusted total: {before} {sign} {delta} = {after}',
|
|
116
|
+
previewExpression: 'Calculated total: {formula} = {after}',
|
|
117
|
+
errAdjustInvalid: 'Check the adjustment: put a number after + or -, e.g. +500 or -200.',
|
|
118
|
+
errAdjustRange: 'The adjusted total must be greater than 0 and within the allowed range.',
|
|
114
119
|
errPeriods: 'Installments must be a whole number from 1 to 120.',
|
|
115
120
|
errDate: 'Choose a valid due date.',
|
|
116
121
|
noticeAdded: 'Added “{name}” — {n} periods.',
|
|
@@ -273,6 +278,11 @@ window.__ModuleLoader__.load({
|
|
|
273
278
|
errName: '请输入账单名称。',
|
|
274
279
|
errAmount: '请输入大于 0 的金额,最多两位小数。',
|
|
275
280
|
errAmountSmall: '总金额太小:每期至少需要 0.01 元。',
|
|
281
|
+
amountAdjustHint: '提示:可直接输入算式,如 3994.28+100.00,或 +100 / -200 在当前总额上增减。',
|
|
282
|
+
previewAdjust: '调整后总额:{before} {sign} {delta} = {after}',
|
|
283
|
+
previewExpression: '计算结果:{formula} = {after}',
|
|
284
|
+
errAdjustInvalid: '调整金额格式不正确:请在 + 或 - 后输入数字,例如 +500、-200。',
|
|
285
|
+
errAdjustRange: '调整后的总金额需大于 0 且不超过上限。',
|
|
276
286
|
errPeriods: '分期期数需为 1–120 的整数。',
|
|
277
287
|
errDate: '请选择有效的还款日期。',
|
|
278
288
|
noticeAdded: '已添加「{name}」,共 {n} 期。',
|
|
@@ -469,6 +479,7 @@ window.__ModuleLoader__.load({
|
|
|
469
479
|
'.il-segment-item.is-on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:600;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l1)}',
|
|
470
480
|
'.il-segment-item:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
471
481
|
'.il-preview{display:flex;flex-direction:column;gap:4px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);font-size:12.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}',
|
|
482
|
+
'.il-hint{font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-secondary)}',
|
|
472
483
|
'.il-preview-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
473
484
|
'.il-preview-main{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
474
485
|
'.il-error{padding:8px 12px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;color:var(--dsw-alias-state-error-primary);font-size:12.5px;line-height:1.45}',
|
|
@@ -733,6 +744,79 @@ window.__ModuleLoader__.load({
|
|
|
733
744
|
return cents;
|
|
734
745
|
}
|
|
735
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Parse the amount field. Supported forms (amount literals accept thousands
|
|
749
|
+
* separators, full-width signs and spaces):
|
|
750
|
+
* - "1200.00" → absolute amount
|
|
751
|
+
* - "+300" / "-200" → adjust the current bill total (edit mode)
|
|
752
|
+
* - "3994.28+100.00" → arithmetic expression, saved as its result
|
|
753
|
+
* - "3994.28-100.00", "1000+200-50", "+100-50" (relative to the total)
|
|
754
|
+
* Returns { mode, ok, cents, deltaCents, reason? }; `cents` is the effective
|
|
755
|
+
* total in cents and is never trusted when `ok` is false.
|
|
756
|
+
*/
|
|
757
|
+
const AMOUNT_TERM_PATTERN = /([+-]?)([0-9]+(?:\.[0-9]{1,2})?)/g;
|
|
758
|
+
function parseAmountInput(text, baseCents) {
|
|
759
|
+
const raw = typeof text === 'string' ? text.trim() : '';
|
|
760
|
+
if (!raw) return { mode: 'absolute', ok: false, cents: 0, deltaCents: 0 };
|
|
761
|
+
// Normalize full-width operators and drop separators so "3,994.28 + 100"
|
|
762
|
+
// parses as the formula the user typed.
|
|
763
|
+
const compact = raw.replace(/+/g, '+').replace(/[-−]/g, '-').replace(/[\s,,]/g, '');
|
|
764
|
+
const leadingSign = compact.charAt(0) === '+' || compact.charAt(0) === '-';
|
|
765
|
+
const terms = [];
|
|
766
|
+
let consumed = 0;
|
|
767
|
+
let match;
|
|
768
|
+
AMOUNT_TERM_PATTERN.lastIndex = 0;
|
|
769
|
+
while ((match = AMOUNT_TERM_PATTERN.exec(compact)) !== null) {
|
|
770
|
+
if (match.index !== consumed) {
|
|
771
|
+
terms.length = 0;
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
// Every term after the first must carry its own operator; otherwise an
|
|
775
|
+
// invalid literal like "100+1.234" would split into "1.23" and "4".
|
|
776
|
+
if (terms.length > 0 && match[1] === '') {
|
|
777
|
+
terms.length = 0;
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
const cents = parseAmountToCents(match[2]);
|
|
781
|
+
if (!(cents > 0)) {
|
|
782
|
+
terms.length = 0;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
terms.push({ sign: match[1] === '-' ? -1 : 1, cents });
|
|
786
|
+
consumed += match[0].length;
|
|
787
|
+
}
|
|
788
|
+
if (consumed !== compact.length) terms.length = 0;
|
|
789
|
+
if (terms.length === 0) {
|
|
790
|
+
if (leadingSign) {
|
|
791
|
+
return { mode: 'adjust', ok: false, reason: 'invalid', cents: 0, deltaCents: 0 };
|
|
792
|
+
}
|
|
793
|
+
if (/[+-]/.test(compact.slice(1))) {
|
|
794
|
+
return { mode: 'expression', ok: false, reason: 'invalid', cents: 0, deltaCents: 0 };
|
|
795
|
+
}
|
|
796
|
+
return { mode: 'absolute', ok: false, cents: 0, deltaCents: 0 };
|
|
797
|
+
}
|
|
798
|
+
const termSum = terms.reduce((sum, term) => sum + term.sign * term.cents, 0);
|
|
799
|
+
if (leadingSign) {
|
|
800
|
+
// The bill's current total is the implicit left operand: "+300" means
|
|
801
|
+
// "current total + 300", "-200" means "current total - 200".
|
|
802
|
+
const base = Number.isSafeInteger(baseCents) && baseCents > 0 ? baseCents : 0;
|
|
803
|
+
const cents = base + termSum;
|
|
804
|
+
if (!(cents > 0) || cents > MAX_TOTAL_CENTS) {
|
|
805
|
+
return { mode: 'adjust', ok: false, reason: 'range', cents, deltaCents: termSum };
|
|
806
|
+
}
|
|
807
|
+
return { mode: 'adjust', ok: true, cents, deltaCents: termSum };
|
|
808
|
+
}
|
|
809
|
+
if (terms.length === 1) {
|
|
810
|
+
return { mode: 'absolute', ok: true, cents: terms[0].cents, deltaCents: 0 };
|
|
811
|
+
}
|
|
812
|
+
// A formula that starts with an amount, e.g. "3994.28+100.00": the typed
|
|
813
|
+
// amounts are the calculation and the bill total becomes the result.
|
|
814
|
+
if (!(termSum > 0) || termSum > MAX_TOTAL_CENTS) {
|
|
815
|
+
return { mode: 'expression', ok: false, reason: 'range', cents: termSum, deltaCents: 0 };
|
|
816
|
+
}
|
|
817
|
+
return { mode: 'expression', ok: true, cents: termSum, deltaCents: 0 };
|
|
818
|
+
}
|
|
819
|
+
|
|
736
820
|
function splitAmount(totalCents, periods) {
|
|
737
821
|
const count = Math.max(1, Math.round(periods));
|
|
738
822
|
const base = Math.floor(totalCents / count);
|
|
@@ -1370,9 +1454,14 @@ window.__ModuleLoader__.load({
|
|
|
1370
1454
|
const platform = hasSource ? importPlatform(input.platform) : null;
|
|
1371
1455
|
// CSV cell names are literal values (a quoted comma must survive a
|
|
1372
1456
|
// round-trip); free-text heuristics still strip separators from names.
|
|
1373
|
-
|
|
1457
|
+
// A row whose name cell is empty falls back to the platform default name
|
|
1458
|
+
// so it is never stored as a nameless bill (which normalizeBill would
|
|
1459
|
+
// silently drop on the next read).
|
|
1460
|
+
const name = (input.rawName
|
|
1374
1461
|
? String(input.name || '').replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 40)
|
|
1375
|
-
: importCleanName(input.name, input.defaultName || '')
|
|
1462
|
+
: importCleanName(input.name, input.defaultName || ''))
|
|
1463
|
+
|| importCleanName(input.defaultName || '', '');
|
|
1464
|
+
if (!name) return null;
|
|
1376
1465
|
const totalCents = Math.round(Number(input.totalCents) || 0);
|
|
1377
1466
|
if (!(totalCents > 0) || totalCents > MAX_TOTAL_CENTS) return null;
|
|
1378
1467
|
let kind = input.kind === 'single' ? 'single' : 'installment';
|
|
@@ -1394,7 +1483,7 @@ window.__ModuleLoader__.load({
|
|
|
1394
1483
|
return {
|
|
1395
1484
|
platform: platform ? platform.id : '',
|
|
1396
1485
|
kind,
|
|
1397
|
-
name
|
|
1486
|
+
name,
|
|
1398
1487
|
totalCents,
|
|
1399
1488
|
periods,
|
|
1400
1489
|
firstDue,
|
|
@@ -1497,9 +1586,15 @@ window.__ModuleLoader__.load({
|
|
|
1497
1586
|
const cells = importSplitDelimited(lines[index], delimiter);
|
|
1498
1587
|
if (cells.length < 2) continue;
|
|
1499
1588
|
const firstCell = importNormalizeCell(cells[0]);
|
|
1500
|
-
// The plugin's export CSV appends a month-summary table after a
|
|
1501
|
-
// separator
|
|
1589
|
+
// The plugin's export CSV appends a derived month-summary table after a
|
|
1590
|
+
// blank separator: zh "还款月份" / en "Month" header followed by YYYY-MM
|
|
1591
|
+
// rows. It is derived data, not bills — stop before parsing it.
|
|
1502
1592
|
if (firstCell.indexOf('还款月份') !== -1) break;
|
|
1593
|
+
if (cells.length === 4
|
|
1594
|
+
&& /^(month|还款月份)$/.test(firstCell)
|
|
1595
|
+
&& /due|paid|应还|已还|待还/.test(importNormalizeCell(cells[1] || ''))) {
|
|
1596
|
+
break;
|
|
1597
|
+
}
|
|
1503
1598
|
if (/^\d{4}-\d{2}$/.test(cells[0].trim()) && cells.length <= 4) break;
|
|
1504
1599
|
// Skip a repeated header row when a table was pasted twice.
|
|
1505
1600
|
if (cells.length === headerCells.length
|
|
@@ -2381,9 +2476,13 @@ window.__ModuleLoader__.load({
|
|
|
2381
2476
|
|
|
2382
2477
|
const money = (cents) => formatMoney(cents, tag);
|
|
2383
2478
|
const isSingle = kind === 'single';
|
|
2384
|
-
|
|
2479
|
+
// While editing, "+300" / "-200" adjust the bill's current total instead of
|
|
2480
|
+
// replacing it; adding a new bill only accepts an absolute amount.
|
|
2481
|
+
const baseCents = editing ? editing.totalCents : 0;
|
|
2482
|
+
const amountParse = parseAmountInput(amount, baseCents);
|
|
2483
|
+
const totalCents = amountParse.ok ? amountParse.cents : 0;
|
|
2385
2484
|
const periodCount = isSingle ? 1 : Number(periods);
|
|
2386
|
-
const previewOk =
|
|
2485
|
+
const previewOk = amountParse.ok && (isSingle
|
|
2387
2486
|
|| (Number.isInteger(periodCount) && periodCount >= 1 && periodCount <= MAX_PERIODS && totalCents >= periodCount));
|
|
2388
2487
|
const split = previewOk && !isSingle ? splitAmount(totalCents, periodCount) : null;
|
|
2389
2488
|
const equal = split !== null && (split.length === 1 || split[0] === split[split.length - 1]);
|
|
@@ -2395,6 +2494,17 @@ window.__ModuleLoader__.load({
|
|
|
2395
2494
|
: equal
|
|
2396
2495
|
? format(t, 'previewEqual', { amount: money(split[0]) })
|
|
2397
2496
|
: format(t, 'previewLast', { amount: money(split[0]), last: money(split[split.length - 1]) });
|
|
2497
|
+
const previewAdjust = amountParse.mode === 'adjust' && amountParse.ok && editing
|
|
2498
|
+
? format(t, 'previewAdjust', {
|
|
2499
|
+
before: money(baseCents),
|
|
2500
|
+
sign: amountParse.deltaCents >= 0 ? '+' : '-',
|
|
2501
|
+
delta: money(Math.abs(amountParse.deltaCents)),
|
|
2502
|
+
after: money(totalCents),
|
|
2503
|
+
})
|
|
2504
|
+
: '';
|
|
2505
|
+
const previewExpression = amountParse.mode === 'expression' && amountParse.ok
|
|
2506
|
+
? format(t, 'previewExpression', { formula: amount.trim(), after: money(totalCents) })
|
|
2507
|
+
: '';
|
|
2398
2508
|
const previewLine = previewOk && firstDate
|
|
2399
2509
|
? (isSingle
|
|
2400
2510
|
? format(t, 'previewSingleLine', { date: formatDate(firstDue, tag) })
|
|
@@ -2402,14 +2512,19 @@ window.__ModuleLoader__.load({
|
|
|
2402
2512
|
+ ' · ' + format(t, 'previewMonthlyDay', { day: firstDate.getDate() }))
|
|
2403
2513
|
: '';
|
|
2404
2514
|
|
|
2515
|
+
const amountErrorKey = () => {
|
|
2516
|
+
if (amountParse.mode === 'absolute') return 'errAmount';
|
|
2517
|
+
return amountParse.reason === 'invalid' ? 'errAdjustInvalid' : 'errAdjustRange';
|
|
2518
|
+
};
|
|
2519
|
+
|
|
2405
2520
|
const submit = (event) => {
|
|
2406
2521
|
if (event && typeof event.preventDefault === 'function') event.preventDefault();
|
|
2407
2522
|
if (!name.trim()) {
|
|
2408
2523
|
setErrorKey('errName');
|
|
2409
2524
|
return;
|
|
2410
2525
|
}
|
|
2411
|
-
if (!
|
|
2412
|
-
setErrorKey(
|
|
2526
|
+
if (!amountParse.ok) {
|
|
2527
|
+
setErrorKey(amountErrorKey());
|
|
2413
2528
|
return;
|
|
2414
2529
|
}
|
|
2415
2530
|
if (!isSingle) {
|
|
@@ -2505,12 +2620,15 @@ window.__ModuleLoader__.load({
|
|
|
2505
2620
|
onChange: (event) => { setName(event.target.value); clearError(); },
|
|
2506
2621
|
})),
|
|
2507
2622
|
h('div', { className: 'il-field-row' },
|
|
2508
|
-
|
|
2509
|
-
className: 'il-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2623
|
+
h('label', { className: 'il-field' },
|
|
2624
|
+
h('span', { className: 'il-field-label' }, t('fieldAmount')),
|
|
2625
|
+
h('input', {
|
|
2626
|
+
className: 'il-input', type: 'text', inputMode: editing ? 'text' : 'decimal', value: amount,
|
|
2627
|
+
placeholder: t('amountPlaceholder'), autoComplete: 'off',
|
|
2628
|
+
'aria-invalid': errorKey === 'errAmount' || errorKey === 'errAdjustInvalid' || errorKey === 'errAdjustRange' || undefined,
|
|
2629
|
+
onChange: (event) => { setAmount(event.target.value); clearError(); },
|
|
2630
|
+
}),
|
|
2631
|
+
editing ? h('span', { className: 'il-hint' }, t('amountAdjustHint')) : null),
|
|
2514
2632
|
isSingle
|
|
2515
2633
|
? field(t('fieldDue'), dateInput)
|
|
2516
2634
|
: field(t('fieldPeriods'), h('input', {
|
|
@@ -2536,6 +2654,8 @@ window.__ModuleLoader__.load({
|
|
|
2536
2654
|
previewOk
|
|
2537
2655
|
? h('div', { className: 'il-preview-main' }, previewMain)
|
|
2538
2656
|
: h('div', null, t('previewHint')),
|
|
2657
|
+
previewOk && previewExpression ? h('div', null, previewExpression) : null,
|
|
2658
|
+
previewOk && previewAdjust ? h('div', null, previewAdjust) : null,
|
|
2539
2659
|
previewOk ? h('div', null, previewLine) : null),
|
|
2540
2660
|
errorKey ? h('div', { className: 'il-error', role: 'alert' }, t(errorKey)) : null,
|
|
2541
2661
|
h('div', { className: 'il-form-actions' },
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-personal-ledger",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Installment ledger: record and edit installment / one-time bills, import JD Baitiao / credit card / Alipay bills, track monthly repayments with one-click payment, and export text / CSV / PNG.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test/run-tests.mjs",
|
|
8
|
+
"check": "node --check index.js && node --check client.js",
|
|
9
|
+
"release": "node scripts/release.mjs"
|
|
10
|
+
},
|
|
6
11
|
"license": "MIT",
|
|
7
12
|
"author": "dsh-personal-ledger contributors",
|
|
8
13
|
"keywords": [
|
|
@@ -27,6 +32,7 @@
|
|
|
27
32
|
"locale/*.json",
|
|
28
33
|
"icon.svg",
|
|
29
34
|
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
30
36
|
"LICENSE"
|
|
31
37
|
],
|
|
32
38
|
"dsh": {
|