这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。 此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。 当前状态(提交前已实测): - index.html 内联薪酬算法已删除,改调 WHTPayroll - shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用 - 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装 未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、 运行时 sqlite 数据与日志、时间戳备份、.trash。
151 lines
7.9 KiB
JavaScript
151 lines
7.9 KiB
JavaScript
// 验收:发薪日标记 + 奖惩终稿修正入口
|
|
const { spawn } = require('child_process');
|
|
const fs = require('fs');
|
|
const CHROME = '/usr/bin/chromium';
|
|
const OUT_DIR = __dirname;
|
|
const URL = 'http://127.0.0.1:8388/index.html';
|
|
|
|
const chrome = spawn(CHROME, [
|
|
'--headless=new','--no-sandbox','--disable-gpu','--disable-dev-shm-usage',
|
|
'--remote-debugging-port=9231', `--user-data-dir=${OUT_DIR}/.chrome-verify`,
|
|
'--window-size=420,1000','about:blank'
|
|
], { stdio: ['ignore','ignore','pipe'] });
|
|
const stderr = []; chrome.stderr.on('data', d => stderr.push(d.toString()));
|
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
async function getWsUrl() {
|
|
for (let i=0;i<40;i++){ try{ const t=await(await fetch('http://127.0.0.1:9231/json/list')).json(); if(t.length)return t[0].webSocketDebuggerUrl; }catch(e){} await sleep(250);} throw new Error('CDP fail: '+stderr.join('').slice(-400));
|
|
}
|
|
(async () => {
|
|
const wsUrl = await getWsUrl();
|
|
const ws = new WebSocket(wsUrl);
|
|
let id=0; const pending=new Map(); const logs=[];
|
|
let openRes; const openP=new Promise(r=>openRes=r); ws.onopen=openRes;
|
|
ws.onmessage=e=>{ const m=JSON.parse(e.data);
|
|
if(m.id&&pending.has(m.id)){pending.get(m.id)(m);pending.delete(m.id);}
|
|
else if(m.method==='Runtime.consoleAPICalled'){const t=(m.params.args||[]).map(a=>a.value??a.description??'').join(' '); if(!/tailwindcss/.test(t))logs.push('C '+t);}
|
|
else if(m.method==='Runtime.exceptionThrown')logs.push('EXC '+JSON.stringify(m.params.exceptionDetails).slice(0,300)); };
|
|
await openP;
|
|
const send=(me,p={})=>new Promise(r=>{const i2=++id;pending.set(i2,r);ws.send(JSON.stringify({id:i2,method:me,params:p}));});
|
|
const ev=async(x)=>{const r=await send('Runtime.evaluate',{expression:x,awaitPromise:true,returnByValue:true});
|
|
if(r.result&&r.result.exceptionDetails)return{__err:JSON.stringify(r.result.exceptionDetails).slice(0,300)};
|
|
return r.result&&r.result.result&&r.result.result.value;};
|
|
|
|
await send('Runtime.enable'); await send('Page.enable');
|
|
|
|
// 预置:2026-08 有打卡+奖惩数据,便于验证上月核对入口
|
|
await send('Page.navigate',{url:URL}); await sleep(1200);
|
|
await ev(`(function(){
|
|
const rec = {};
|
|
for (let d=1; d<=28; d++) {
|
|
rec['2026-08-'+String(d).padStart(2,'0')] = { status:'work', shift_type: d%3?'day':'night', hours: d%3?8:10, contraband_found: d===5?3:0, contraband_missed: d===9?2:0, other_penalty: d===12?50:null, penalty_reason: d===12?'迟到':'' };
|
|
}
|
|
for (let d=1; d<=20; d++) {
|
|
rec['2026-09-'+String(d).padStart(2,'0')] = { status:'work', shift_type:'day', hours:8, contraband_found: d===3?1:0, contraband_missed:0, other_penalty:null, penalty_reason:'' };
|
|
}
|
|
localStorage.setItem('work_records', JSON.stringify(rec));
|
|
localStorage.removeItem('work_month_adjust');
|
|
return 'seeded';
|
|
})()`);
|
|
await send('Page.navigate',{url:URL}); await sleep(2000);
|
|
|
|
// ① 日历发薪日标记
|
|
const cal = await ev(`(function(){
|
|
const marks = Array.from(document.querySelectorAll('.pay-mark')).map(m=>({cls:m.className, txt:m.textContent}));
|
|
const ringA = document.querySelectorAll('.pay-ring-a').length;
|
|
const ringB = document.querySelectorAll('.pay-ring-b').length;
|
|
return { payMarkCount: marks.length, marks: marks.slice(0,6), ringA, ringB };
|
|
})()`);
|
|
console.log('① 发薪日标记:', JSON.stringify(cal));
|
|
|
|
const s1 = await send('Page.captureScreenshot',{format:'png'});
|
|
fs.writeFileSync(OUT_DIR+'/v1_calendar.png', Buffer.from(s1.result.data,'base64'));
|
|
|
|
// ② 点发薪日格子 -> 到账横幅
|
|
const banner = await ev(`new Promise(r=>{
|
|
const cells = Array.from(document.querySelectorAll('.grid.grid-cols-7.gap-1\\\\.5 > div'));
|
|
const target = cells.find(c => c.querySelector('.pay-mark'));
|
|
if(!target) return r({err:'no payday cell'});
|
|
target.click();
|
|
setTimeout(()=>{
|
|
const ov = document.querySelector('.fixed.inset-0.z-50');
|
|
const txt = ov ? ov.innerText.replace(/\\s+/g,' ') : null;
|
|
r({ drawerOpen: !!ov, hasBanner: !!(txt && txt.includes('今日到账')), snippet: txt ? txt.slice(0,110) : null });
|
|
}, 420);
|
|
})`);
|
|
console.log('② 发薪日到账横幅:', JSON.stringify(banner));
|
|
|
|
const s2 = await send('Page.captureScreenshot',{format:'png'});
|
|
fs.writeFileSync(OUT_DIR+'/v2_payday_drawer.png', Buffer.from(s2.result.data,'base64'));
|
|
|
|
// 关抽屉
|
|
await ev(`(function(){ const b=document.querySelector('.fixed.inset-0.z-50 button'); if(b) b.click(); return 1; })()`);
|
|
await sleep(400);
|
|
|
|
// ③ 汇总页 -> 上月核对入口
|
|
const summary = await ev(`new Promise(r=>{
|
|
const t = Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='汇总');
|
|
t.click();
|
|
setTimeout(()=>{
|
|
const btns = Array.from(document.querySelectorAll('button')).filter(b=>/核对修正|查看\\/改稿/.test(b.innerText||''));
|
|
const tags = Array.from(document.querySelectorAll('.stat-tag')).map(x=>x.textContent.trim());
|
|
const body = document.body.innerText.replace(/\\s+/g,' ');
|
|
r({ correctBtnCount: btns.length, tags, hasRawReview: body.includes('日常累计'), hasKoujing: body.includes('奖惩口径') });
|
|
}, 400);
|
|
})`);
|
|
console.log('③ 汇总页修正入口:', JSON.stringify(summary));
|
|
|
|
const s3 = await send('Page.captureScreenshot',{format:'png'});
|
|
fs.writeFileSync(OUT_DIR+'/v3_summary.png', Buffer.from(s3.result.data,'base64'));
|
|
|
|
// ④ 打开上月核对抽屉,填终稿 80,定稿
|
|
const adj = await ev(`new Promise(r=>{
|
|
const btn = Array.from(document.querySelectorAll('button')).filter(b=>/核对修正|查看\\/改稿/.test(b.innerText||''))[0];
|
|
if(!btn) return r({err:'no correct btn'});
|
|
btn.click();
|
|
setTimeout(()=>{
|
|
const ov = document.querySelector('.fixed.inset-0.z-50');
|
|
const txt = ov ? ov.innerText.replace(/\\s+/g,' ') : null;
|
|
const input = ov && ov.querySelector('input[type=number]');
|
|
r({ open: !!ov, hasRawSum: !!(txt&&txt.includes('累计小计')), prefill: input ? input.value : null, snippet: txt?txt.slice(0,150):null });
|
|
}, 420);
|
|
})`);
|
|
console.log('④ 核对抽屉:', JSON.stringify(adj));
|
|
|
|
const s4 = await send('Page.captureScreenshot',{format:'png'});
|
|
fs.writeFileSync(OUT_DIR+'/v4_adjust_drawer.png', Buffer.from(s4.result.data,'base64'));
|
|
|
|
// 填 80 并定稿
|
|
const fin = await ev(`new Promise(r=>{
|
|
const ov = document.querySelector('.fixed.inset-0.z-50');
|
|
const input = ov.querySelector('input[type=number]');
|
|
input.value = '80';
|
|
input.dispatchEvent(new Event('input',{bubbles:true}));
|
|
setTimeout(()=>{
|
|
const delta = ov.innerText.replace(/\\s+/g,' ');
|
|
const okBtn = Array.from(ov.querySelectorAll('button')).find(b=>/确认定稿/.test(b.innerText||''));
|
|
okBtn.click();
|
|
setTimeout(()=>{
|
|
const store = JSON.parse(localStorage.getItem('work_month_adjust')||'{}');
|
|
const body = document.body.innerText.replace(/\\s+/g,' ');
|
|
r({ deltaShown: /上调|下调/.test(delta), stored: store, bodyHasFinal: body.includes('已定稿') });
|
|
}, 500);
|
|
}, 300);
|
|
})`);
|
|
console.log('⑤ 定稿写入:', JSON.stringify(fin));
|
|
|
|
const s5 = await send('Page.captureScreenshot',{format:'png'});
|
|
fs.writeFileSync(OUT_DIR+'/v5_after_final.png', Buffer.from(s5.result.data,'base64'));
|
|
|
|
// ⑥ 定稿后发放口径是否跟着变(切到 9 月看 9月10日到账)
|
|
const payout = await ev(`(function(){
|
|
const body = document.body.innerText.replace(/\\s+/g,' ');
|
|
const m = body.match(/9月10日[^¥]*¥([\\d,.]+)/);
|
|
return { snippet: body.slice(body.indexOf('本月到账'), body.indexOf('本月到账')+160), matched: m?m[1]:null };
|
|
})()`);
|
|
console.log('⑥ 发放口径:', JSON.stringify(payout));
|
|
|
|
console.log('\n=== 异常/控制台 ===');
|
|
console.log(logs.length?logs.join('\n'):'(无异常)');
|
|
ws.close(); chrome.kill(); process.exit(0);
|
|
})().catch(e=>{ console.error('FAILED:',e.message); console.error(stderr.join('').slice(-400)); try{chrome.kill();}catch(_){}process.exit(1); });
|