From 9efd9a3099f2d75397074121fc59bd27fa801729 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 16 Sep 2026 03:04:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=91=E9=A2=9D=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E4=BF=9D=E7=95=99=E5=8E=9F=E5=A7=8B=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=B2=BE=E5=BA=A6=EF=BC=8C=E5=8E=BB=E9=99=A4=20toFixe?= =?UTF-8?q?d(1)=20=E8=BF=9B=E4=BD=8D=E6=88=AA=E6=96=AD=E5=B9=B6=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E8=87=B3=20v1.0.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++ README.md | 9 +++++ android/app/build.gradle | 4 +- package.json | 2 +- shared/payroll.js | 26 ++++++++----- test/baseline/payroll-baseline.json | 60 ++++++++++++++--------------- test/payroll.test.js | 24 ++++++------ 7 files changed, 80 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6a3a2f..4cf41aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## 七、 版本发布历史 (Changelog) +### v1.0.3 (2026-09-16) +- **修复 (金额精度保留与无损展示)**: + - 去除金额展示层原有的 `toFixed(1)` 进位截断,彻底解决如 `103.5h × ¥19.5/h = ¥2018.25` 在看板与汇总中被四舍五入进位显示为 `¥2018.3` 的问题。 + - 金额计算与格式化逻辑(`money`、`signed`、`moneyParts`)调整为保留原始数据有效位数,同时消除 JS 浮点二进制精度抖动。 + - 看板大字整数与小数拆分逻辑自适应原始小数位,保证整数部千分位与小数分位严密吻合。 +- **Android 原生资产与版本**: + - 同步静态资产至 `android/app/src/main/assets/www/`。 + - 版本号升级:`versionCode 4`,`versionName "1.0.3"`。 + ### v1.0.2 (2026-09-15) - **修复 (MCP 状态联动与生命周期)**: - 修复退出登录后前端 `mcp` 响应式对象残留已开启状态、点击关闭弹出「未登录」错误的问题。 diff --git a/README.md b/README.md index 6c2411e..2215203 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,15 @@ curl -s http://127.0.0.1:9522/api/health ## 七、 版本发布历史 (Changelog) +### v1.0.3 (2026-09-16) +- **修复 (金额精度保留与无损展示)**: + - 去除金额展示层原有的 `toFixed(1)` 进位截断,彻底解决如 `103.5h × ¥19.5/h = ¥2018.25` 在看板与汇总中被四舍五入进位显示为 `¥2018.3` 的问题。 + - 金额计算与格式化逻辑(`money`、`signed`、`moneyParts`)调整为保留原始数据有效位数,同时消除 JS 浮点二进制精度抖动。 + - 看板大字整数与小数拆分逻辑自适应原始小数位,保证整数部千分位与小数分位严密吻合。 +- **Android 原生资产与版本**: + - 同步静态资产至 `android/app/src/main/assets/www/`。 + - 版本号升级:`versionCode 4`,`versionName "1.0.3"`。 + ### v1.0.2 (2026-09-15) - **修复 (MCP 状态联动与生命周期)**: - 修复退出登录后前端 `mcp` 响应式对象残留已开启状态、点击关闭弹出「未登录」错误的问题。 diff --git a/android/app/build.gradle b/android/app/build.gradle index e86b4cd..9e923aa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "top.ouaone.workhours" minSdk 23 targetSdk 34 - versionCode 3 - versionName "1.0.2" + versionCode 4 + versionName "1.0.3" } signingConfigs { diff --git a/package.json b/package.json index ef16d0c..8bc0fe8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "work-hours-tracker", - "version": "1.0.2", + "version": "1.0.3", "private": true, "description": "工时记账:单文件前端 + 薪酬纯模块(shared/payroll.js)+ 平台无关服务端(server/)", "scripts": { diff --git a/shared/payroll.js b/shared/payroll.js index 0de6c8e..97b9efc 100644 --- a/shared/payroll.js +++ b/shared/payroll.js @@ -287,26 +287,32 @@ } // ===== 格式化 ===== - function fmt(n) { return (Number(n) || 0).toFixed(1); } - function money(n) { return '¥' + (Number(n) || 0).toFixed(1); } - function signed(n) { + // 消除浮点二进制抖动,同时保留原始数据(不进行有损进位截断) + function rawAmount(n) { var v = Number(n) || 0; - return (v >= 0 ? '+¥' : '-¥') + Math.abs(v).toFixed(1); + return Math.round((v + Number.EPSILON) * 100) / 100; + } + function fmt(n) { return (Number(n) || 0).toFixed(1); } + function money(n) { return '¥' + rawAmount(n); } + function signed(n) { + var v = rawAmount(n); + return (v >= 0 ? '+¥' : '-¥') + Math.abs(v); } /** * 金额拆成 符号/整数部/小数部,供看板主数字分层渲染(大字只放整数部)。 - * 必须从 toFixed(1) 的结果上切,不能用 Math.floor 另算, - * 否则 99.96 会被 money() 进为 100.0、而拆分出 99.0,两处数字对不上。 + * 保留原始数据的小数部分,不进行有损四舍五入。 */ function moneyParts(n) { - var v = Number(n) || 0; - var s = Math.abs(v).toFixed(1); + var v = rawAmount(n); + var s = String(Math.abs(v)); var dot = s.indexOf('.'); + var intPart = dot !== -1 ? s.slice(0, dot) : s; + var fracPart = dot !== -1 ? s.slice(dot) : ''; return { sign: v < 0 ? '-' : '', - int: s.slice(0, dot).replace(/\B(?=(\d{3})+(?!\d))/g, ','), // 千分位 - frac: s.slice(dot) + int: intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','), // 千分位 + frac: fracPart }; } diff --git a/test/baseline/payroll-baseline.json b/test/baseline/payroll-baseline.json index fd43109..9584c0c 100644 --- a/test/baseline/payroll-baseline.json +++ b/test/baseline/payroll-baseline.json @@ -110,7 +110,7 @@ "isCurrentMonth": true, "recordCount": 4, "boardInt": "702", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -226,7 +226,7 @@ "isCurrentMonth": true, "recordCount": 2, "boardInt": "468", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -342,7 +342,7 @@ "isCurrentMonth": true, "recordCount": 1, "boardInt": "195", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -458,7 +458,7 @@ "isCurrentMonth": true, "recordCount": 27, "boardInt": "4,428", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -574,7 +574,7 @@ "isCurrentMonth": true, "recordCount": 26, "boardInt": "4,056", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -704,7 +704,7 @@ "isCurrentMonth": true, "recordCount": 3, "boardInt": "468", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -834,7 +834,7 @@ "isCurrentMonth": true, "recordCount": 3, "boardInt": "408", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -950,7 +950,7 @@ "isCurrentMonth": true, "recordCount": 1, "boardInt": "156", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -1066,7 +1066,7 @@ "isCurrentMonth": true, "recordCount": 1, "boardInt": "176", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -1182,7 +1182,7 @@ "isCurrentMonth": true, "recordCount": 6, "boardInt": "312", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -1298,7 +1298,7 @@ "isCurrentMonth": true, "recordCount": 6, "boardInt": "312", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -1414,7 +1414,7 @@ "isCurrentMonth": false, "recordCount": 3, "boardInt": "156", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 31, @@ -1530,7 +1530,7 @@ "isCurrentMonth": false, "recordCount": 3, "boardInt": "156", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 31, @@ -1654,7 +1654,7 @@ "isCurrentMonth": true, "recordCount": 1, "boardInt": "117", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -1778,7 +1778,7 @@ "isCurrentMonth": true, "recordCount": 1, "boardInt": "283", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "-", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -1894,7 +1894,7 @@ "isCurrentMonth": true, "recordCount": 4, "boardInt": "156", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2010,7 +2010,7 @@ "isCurrentMonth": true, "recordCount": 1, "boardInt": "156", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2126,7 +2126,7 @@ "isCurrentMonth": true, "recordCount": 3, "boardInt": "156", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2242,7 +2242,7 @@ "isCurrentMonth": true, "recordCount": 20, "boardInt": "6,400", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2358,7 +2358,7 @@ "isCurrentMonth": true, "recordCount": 1, "boardInt": "156", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2494,7 +2494,7 @@ "isCurrentMonth": true, "recordCount": 4, "boardInt": "624", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2610,7 +2610,7 @@ "isCurrentMonth": true, "recordCount": 0, "boardInt": "0", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2726,7 +2726,7 @@ "isCurrentMonth": true, "recordCount": 3, "boardInt": "516", - "boardFrac": ".8", + "boardFrac": ".75", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2842,7 +2842,7 @@ "isCurrentMonth": true, "recordCount": 3, "boardInt": "429", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -2958,7 +2958,7 @@ "isCurrentMonth": true, "recordCount": 3, "boardInt": "439", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -3074,7 +3074,7 @@ "isCurrentMonth": true, "recordCount": 27, "boardInt": "4,428", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -3190,7 +3190,7 @@ "isCurrentMonth": true, "recordCount": 27, "boardInt": "4,478", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, @@ -3306,7 +3306,7 @@ "isCurrentMonth": false, "recordCount": 2, "boardInt": "351", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 31, @@ -3422,7 +3422,7 @@ "isCurrentMonth": false, "recordCount": 2, "boardInt": "351", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 31, @@ -3538,7 +3538,7 @@ "isCurrentMonth": true, "recordCount": 0, "boardInt": "0", - "boardFrac": ".0", + "boardFrac": "", "boardSign": "", "hourOptions": "6,6.5,7,7.5,8,8.5,9,9.5,10,10.5,11,11.5,12", "daysInMonth": 30, diff --git a/test/payroll.test.js b/test/payroll.test.js index 0f1a686..5dacf49 100644 --- a/test/payroll.test.js +++ b/test/payroll.test.js @@ -425,20 +425,22 @@ test('isCurrentMonth / isSameDay 只认传进来的 now', () => { // ───────────────────────── 格式化 ───────────────────────── -test('金额格式化保留 1 位,null/undefined 当 0', () => { +test('金额格式化保留原始数据(分位不截断,不进位),null/undefined 当 0', () => { assert.equal(P.fmt(undefined), '0.0'); - assert.equal(P.money(null), '¥0.0'); - assert.equal(P.money(1234.56), '¥1234.6'); - assert.equal(P.signed(0), '+¥0.0'); - assert.equal(P.signed(-5), '-¥5.0'); - assert.equal(P.signed(30), '+¥30.0'); + assert.equal(P.money(null), '¥0'); + assert.equal(P.money(1234.56), '¥1234.56'); + assert.equal(P.money(2018.25), '¥2018.25'); + assert.equal(P.signed(0), '+¥0'); + assert.equal(P.signed(-5), '-¥5'); + assert.equal(P.signed(-50.25), '-¥50.25'); + assert.equal(P.signed(30), '+¥30'); }); -test('moneyParts 从 toFixed(1) 的结果上切,不另算整数部', () => { - // 99.96 若用 Math.floor 另算会得到 99,与 money() 的 ¥100.0 打架 - assert.deepEqual(P.moneyParts(99.96), { sign: '', int: '100', frac: '.0' }); - assert.deepEqual(P.moneyParts(-1234.56), { sign: '-', int: '1,234', frac: '.6' }); - assert.deepEqual(P.moneyParts(0), { sign: '', int: '0', frac: '.0' }); +test('moneyParts 保留原始数据小数位,千分位与小数部拆分对齐', () => { + assert.deepEqual(P.moneyParts(99.96), { sign: '', int: '99', frac: '.96' }); + assert.deepEqual(P.moneyParts(2018.25), { sign: '', int: '2,018', frac: '.25' }); + assert.deepEqual(P.moneyParts(-1234.56), { sign: '-', int: '1,234', frac: '.56' }); + assert.deepEqual(P.moneyParts(0), { sign: '', int: '0', frac: '' }); assert.deepEqual(P.moneyParts(1234567.8), { sign: '', int: '1,234,567', frac: '.8' }); });