这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。 此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。 当前状态(提交前已实测): - index.html 内联薪酬算法已删除,改调 WHTPayroll - shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用 - 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装 未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、 运行时 sqlite 数据与日志、时间戳备份、.trash。
102 lines
4.7 KiB
JavaScript
102 lines
4.7 KiB
JavaScript
// 决定性探针 v2:深入 __vue_app__ 组件实例树读 settingsOpen/tab
|
||
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=9229', `--user-data-dir=${OUT_DIR}/.chrome-test5`,
|
||
'--window-size=420,900','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:9229/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 () => {
|
||
try {
|
||
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=ev=>{ const m=JSON.parse(ev.data);
|
||
if(m.id&&pending.has(m.id)){pending.get(m.id)(m);pending.delete(m.id);}
|
||
else if(m.method==='Runtime.consoleAPICalled')logs.push('C '+(m.params.args||[]).map(a=>a.value??a.description??'').join(' '));
|
||
else if(m.method==='Runtime.exceptionThrown')logs.push('EXC '+JSON.stringify(m.params.exceptionDetails)); };
|
||
await openP;
|
||
const send=(method,params={})=>new Promise(res=>{const mid=++id;pending.set(mid,res);ws.send(JSON.stringify({id:mid,method,params}));});
|
||
const ev=async(expr)=>{const r=await send('Runtime.evaluate',{expression:expr,awaitPromise:true,returnByValue:true}); if(r.result&&r.result.exceptionDetails)return{__err:JSON.stringify(r.result.exceptionDetails)}; return r.result&&r.result.result&&r.result.result.value;};
|
||
|
||
await send('Runtime.enable'); await send('Page.enable');
|
||
await send('Page.navigate',{url:URL}); await sleep(2200);
|
||
|
||
// 深入 __vue_app__ 拿 setupState(Vue3:app._instance.setupState / 通过组件的 exposed)
|
||
const probe = await ev(`
|
||
(function(){
|
||
const appEl = document.querySelector('#app');
|
||
const app = appEl.__vue_app__;
|
||
const out = {};
|
||
// vue 3.3+: app._instance (root instance)
|
||
const root = app._instance;
|
||
out.hasRootInstance = !!root;
|
||
if (root) {
|
||
const setup = root.setupState;
|
||
const ctx = root.ctx;
|
||
out.setupKeys = setup ? Object.keys(setup) : null;
|
||
const get = (name) => {
|
||
// ref 或 getter
|
||
const v = setup ? setup[name] : undefined;
|
||
if (v === undefined && ctx) return ctx[name];
|
||
return v;
|
||
};
|
||
function deref(v) {
|
||
if (v === undefined || v === null) return v;
|
||
if (typeof v === 'object' && '_value' in v) return v._value; // ref
|
||
return v;
|
||
}
|
||
const sOpen = deref(get('settingsOpen'));
|
||
const tabVal = deref(get('tab'));
|
||
const iso = deref(get('isDrawerOpen'));
|
||
out.settingsOpen = sOpen;
|
||
out.isDrawerOpen = iso;
|
||
out.tab = tabVal && tabVal._value !== undefined ? tabVal._value : tabVal;
|
||
out.tab_raw = tabVal;
|
||
} else {
|
||
// 尝试通过子组件
|
||
const sub = app._container && app._container.__vue_app__;
|
||
out.subSearched = !!sub;
|
||
}
|
||
return out;
|
||
})()
|
||
`);
|
||
console.log('深度读取:', JSON.stringify(probe, null, 2));
|
||
|
||
// 现在点击设置,再看 settingsOpen
|
||
const clickThen = await ev(`
|
||
new Promise(r => {
|
||
const btn = Array.from(document.querySelectorAll('nav button')).find(b=>(b.innerText||'').trim()==='设置');
|
||
if (!btn) return r({err:'no btn'});
|
||
btn.click();
|
||
setTimeout(() => {
|
||
const appEl = document.querySelector('#app');
|
||
const root = appEl.__vue_app__._instance;
|
||
const setup = root.setupState;
|
||
function deref(v){ if(v&&typeof v==='object'&&('_value' in v))return v._value; return v; }
|
||
r({
|
||
settingsOpen_afterClick: deref(setup.settingsOpen),
|
||
tab_afterClick: deref(setup.tab) && deref(setup.tab)._value!==undefined ? deref(setup.tab)._value : deref(setup.tab),
|
||
z50: document.querySelectorAll('.z-50').length
|
||
});
|
||
}, 400);
|
||
})
|
||
`);
|
||
console.log('点击设置后:', JSON.stringify(clickThen, null, 2));
|
||
|
||
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); }
|
||
})();
|