Files
ERP/ofbiz-framework/plugins/modern-ui/app/scripts/verify-scroll-check.mjs
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

82 lines
4.4 KiB
JavaScript

import { spawn } from 'node:child_process'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { setTimeout as delay } from 'node:timers/promises'
const CHROME = process.env.CHROME_BIN || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
const URL = process.env.URL || 'http://localhost:8080/modern/app/#/parties'
const OUT = process.env.OUT || '/Users/qiu/Desktop/ERP/ofbiz-framework/plugins/modern-ui/verification/scroll-check.png'
const profile = await mkdtemp(path.join(os.tmpdir(), 'scrollchk.'))
const chrome = spawn(CHROME, ['--headless=new', '--disable-gpu', '--no-first-run', '--remote-debugging-port=0', '--window-size=1280,900', `--user-data-dir=${profile}`, 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] })
let port = ''
const deadline = Date.now() + 15000
while (Date.now() < deadline && !port) {
try { const t = await readFile(path.join(profile, 'DevToolsActivePort'), 'utf8'); port = t.trim().split('\n')[0] } catch { await delay(150) }
}
const targets = await fetch(`http://127.0.0.1:${port}/json/list`).then((r) => r.json())
const ws = new WebSocket(targets.find((t) => t.type === 'page' && t.webSocketDebuggerUrl).webSocketDebuggerUrl)
await new Promise((res) => ws.addEventListener('open', res, { once: true }))
let id = 0
const pending = new Map()
ws.addEventListener('message', (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } })
const cmd = (method, params = {}) => new Promise((res) => { const i = ++id; pending.set(i, res); ws.send(JSON.stringify({ id: i, method, params })) })
const evalJs = async (expr) => (await cmd('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })).result?.value
await cmd('Page.enable'); await cmd('Network.enable')
// Log in inside the page (same-origin fetch sets the session cookie), then load the target.
await cmd('Page.navigate', { url: 'http://localhost:8080/modern/app/' })
await delay(1500)
await evalJs(`fetch('/api/v1/login',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:'admin',password:'ADMIN'})}).then(r=>r.status)`)
await delay(800)
await evalJs("window.location.hash = '#/parties'; true")
await cmd('Page.reload', { ignoreCache: true })
// wait for shell + table
const wd = Date.now() + 20000
while (Date.now() < wd) {
const ok = await evalJs("Boolean(document.querySelector('.erp-shell__main') && document.querySelector('.erp-shell__aside') && document.body.innerText.includes('客户与组织管理台'))").catch(() => false)
if (ok) break
await delay(300)
}
await delay(1200)
const before = await evalJs(`(() => {
const aside = document.querySelector('.erp-shell__aside');
const main = document.querySelector('.erp-shell__main');
return {
docScrollable: document.documentElement.scrollHeight > window.innerHeight + 2,
mainScrollable: main.scrollHeight > main.clientHeight + 2,
asideTop: Math.round(aside.getBoundingClientRect().top),
asideBottom: Math.round(aside.getBoundingClientRect().bottom),
viewportH: window.innerHeight,
mainScrollTop: main.scrollTop
};
})()`)
// scroll the main area down
await evalJs("document.querySelector('.erp-shell__main').scrollTop = 100000; true")
await delay(500)
const after = await evalJs(`(() => {
const aside = document.querySelector('.erp-shell__aside');
const main = document.querySelector('.erp-shell__main');
return {
docScrollTop: document.documentElement.scrollTop || document.body.scrollTop,
mainScrollTop: Math.round(main.scrollTop),
asideTop: Math.round(aside.getBoundingClientRect().top),
asideBottom: Math.round(aside.getBoundingClientRect().bottom),
brandVisible: Boolean(document.querySelector('.erp-brand'))
};
})()`)
const shot = await cmd('Page.captureScreenshot', { format: 'png' })
await writeFile(OUT, Buffer.from(shot.data, 'base64'))
console.log(JSON.stringify({ before, after, screenshot: OUT }, null, 2))
// verdict
const pass = !before.docScrollable && before.mainScrollable && after.mainScrollTop > 50 && after.asideTop === 0 && (after.docScrollTop || 0) === 0 && after.asideBottom >= before.viewportH - 2
console.log('SCROLL-FIX:', pass ? 'PASS (sidebar fixed, only main scrolls)' : 'CHECK')
ws.close(); chrome.kill('SIGTERM'); await delay(300); await rm(profile, { recursive: true, force: true })
process.exit(0)