baseline: 薪酬逻辑收口到 shared/payroll.js 后的当前全量状态

这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。

此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。

当前状态(提交前已实测):
- index.html 内联薪酬算法已删除,改调 WHTPayroll
- shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用
- 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装

未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、
运行时 sqlite 数据与日志、时间戳备份、.trash。
This commit is contained in:
OpenClaw Agent
2026-09-05 14:35:00 +08:00
parent 7d48c4ac65
commit 2329955e32
94 changed files with 12770 additions and 237 deletions
+122
View File
@@ -0,0 +1,122 @@
// 在 Node 里跑 index.html 内联的那坨 setup(),拿到它返回的全部 computed。
//
// 这个文件是**唯一**焊在 index.html 结构上的东西:它靠正则抠出内联 <script>,
// 一旦前端拆成 SFC 就会立刻失效。这是刻意的——它的职责只剩「重构前后行为一致」对照,
// 前端拆完那天连同 app-parity 测试一起删掉,而 shared/payroll.js 上的那套断言不受影响。
//
// 与 verify_payroll.js 里那份内联桩件的区别:
// 1. Date 可冻结 → payDayAPassed / isCurrentMonth / cellClass 这些吃「今天」的输出变成确定值,
// 否则基准 JSON 每天跑出来都不一样,根本没法当基准。
// 2. 会把 shared/payroll.js 先注入沙箱,让重构后的 index.html 找得到 WHTPayroll。
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ROOT = path.join(__dirname, '..', '..');
const HTML = path.join(ROOT, 'index.html');
const PAYROLL = path.join(ROOT, 'shared', 'payroll.js');
/** 冻结「现在」:无参 new Date() 与 Date.now() 都回到固定时刻,带参构造照常。 */
function frozenDate(iso) {
const FIXED = new Date(iso).getTime();
return class FrozenDate extends Date {
constructor(...args) {
if (args.length === 0) super(FIXED);
else super(...args);
}
static now() { return FIXED; }
};
}
/** 极简 Vue 桩件:测试只需要能读写与求值,不需要真响应式。 */
function makeVue() {
const mounted = [];
const Vue = {
ref: (v) => ({ value: v }),
reactive: (o) => o,
computed: (fn) => ({ get value() { return fn(); } }),
watch: () => {},
onMounted: (fn) => mounted.push(fn),
createApp(opts) {
Vue._captured = opts.setup();
return { mount: () => Vue._captured };
}
};
return { Vue, mounted };
}
function makeStore(seed) {
const data = { ...seed };
return {
getItem: (k) => (k in data ? data[k] : null),
setItem: (k, v) => { data[k] = v; },
removeItem: (k) => { delete data[k]; },
_raw: () => data,
_records: () => JSON.parse(data.work_records || '{}'),
_settings: () => (data.work_settings ? JSON.parse(data.work_settings) : null),
_adjust: () => (data.work_month_adjust ? JSON.parse(data.work_month_adjust) : null)
};
}
/**
* 载入应用。
* @param {object} o
* @param {object} o.records 打卡记录,key 'YYYY-MM-DD'
* @param {number} o.year 当前查看年
* @param {number} o.month 当前查看月(0–11)
* @param {object} [o.settings] 覆盖设置,null/undefined 表示用默认
* @param {object} [o.adjust] 月度奖惩终稿,key 'YYYY-MM'
* @param {string} [o.now] 冻结的「现在」(ISO)
*/
function loadApp({ records = {}, year, month, settings = null, adjust = null, now = null }) {
const seed = { work_records: JSON.stringify(records) };
if (settings) seed.work_settings = JSON.stringify(settings);
if (adjust) seed.work_month_adjust = JSON.stringify(adjust);
const localStorage = makeStore(seed);
const { Vue, mounted } = makeVue();
const DateImpl = now ? frozenDate(now) : Date;
const sandbox = {
Vue, localStorage, console,
Date: DateImpl, JSON, Math, Number, RegExp, Object, Array, String, Boolean, Error,
navigator: {},
document: {
createElement: () => ({ style: {}, select() {}, click() {} }),
body: { appendChild() {}, removeChild() {} },
getElementById: () => null,
addEventListener() {}
},
confirm: () => true,
setTimeout: (fn) => { if (typeof fn === 'function') fn(); return 0; },
Blob: function () {},
URL: { createObjectURL: () => 'blob:x', revokeObjectURL() {} },
// 刻意不提供 window.WHTSync:测试跑「未登录/纯离线」路径,
// 顺带验证同步缺席时算钱逻辑完全不受影响。
location: { origin: 'http://test' },
fetch: () => Promise.reject(new Error('offline in test'))
};
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
vm.createContext(sandbox);
// 先注入纯模块:重构后的 index.html 依赖它。重构前注入也无害(没人引用)。
if (fs.existsSync(PAYROLL)) {
vm.runInContext(fs.readFileSync(PAYROLL, 'utf8'), sandbox, { filename: 'shared/payroll.js' });
}
const src = fs.readFileSync(HTML, 'utf8');
const m = src.match(/<script>\s*([\s\S]*?)\s*<\/script>\s*<\/body>/);
if (!m) throw new Error('index.html 里没找到应用内联 <script>,harness 需要更新');
vm.runInContext(m[1], sandbox, { filename: 'index.html#app' });
mounted.forEach((fn) => fn()); // 触发 records / settings / adjust 载入
const app = Vue._captured;
app.currentYear.value = year;
app.currentMonth.value = month;
return { app, localStorage, sandbox };
}
module.exports = { loadApp, frozenDate, ROOT };