// 第三轮补拍:柔性甘特 / 世界对比 / 利用率(复制现场世界,修复页签滚动) import { spawn } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; const ROOT = path.resolve(import.meta.dirname, '..'); const require = createRequire(path.join(ROOT, 'apps', 'web', 'package.json')); const { chromium } = require('playwright'); const SHOTS = path.join(ROOT, '_shots'); const TMP = path.join(SHOTS, 'tmp3'); fs.rmSync(TMP, { recursive: true, force: true }); fs.mkdirSync(TMP, { recursive: true }); const PY = path.join(ROOT, '.venv', 'Scripts', 'python.exe'); const VITE = path.join(ROOT, 'apps', 'web', 'node_modules', 'vite', 'bin', 'vite.js'); const API = 'http://127.0.0.1:8100'; const UI = 'http://localhost:5399'; for (const f of ['world.json', 'master.db', 'approvals.json', 'knowledge.json', 'config.json']) { const src = path.join(ROOT, 'server', 'data', f); if (fs.existsSync(src)) fs.copyFileSync(src, path.join(TMP, f)); } const children = []; function run(name, cmd, args, opts = {}) { const log = fs.createWriteStream(path.join(SHOTS, `${name}3.log`)); const p = spawn(cmd, args, { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'], ...opts }); p.stdout.pipe(log); p.stderr.pipe(log); children.push(p); } function cleanup() { for (const p of children) { try { spawn('taskkill', ['/PID', String(p.pid), '/T', '/F'], { stdio: 'ignore' }); } catch {} } } process.on('exit', cleanup); async function waitHttp(url, timeoutMs) { const t0 = Date.now(); while (Date.now() - t0 < timeoutMs) { try { const r = await fetch(url); if (r.status < 500) return; } catch {} await new Promise(r => setTimeout(r, 1000)); } throw new Error('waitHttp timeout: ' + url); } run('backend', PY, ['-m', 'uvicorn', 'server.main:app', '--port', '8100'], { env: { ...process.env, APS_PORT: '8100', APS_WORLD_PATH: path.join(TMP, 'world.json'), APS_DB_PATH: path.join(TMP, 'master.db'), APS_APPROVAL_PATH: path.join(TMP, 'approvals.json'), APS_KNOWLEDGE_PATH: path.join(TMP, 'knowledge.json'), APS_WEB_DEMO_SEED: '1', }, }); run('frontend', process.execPath, [VITE, '--port', '5399', '--strictPort'], { cwd: path.join(ROOT, 'apps', 'web'), env: { ...process.env, VITE_API_TARGET: API }, }); console.log('等待服务…'); await waitHttp(API + '/api/health', 120_000); await waitHttp(UI, 120_000); const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1.5 }); async function shot(name) { await page.waitForTimeout(700); await page.screenshot({ path: path.join(SHOTS, name + '.png') }); console.log(' ✓', name); } // 登录 await page.goto(UI); await page.locator('.guest-login-trigger').waitFor({ state: 'visible', timeout: 60_000 }); await page.locator('.guest-login-trigger').click(); const dialog = page.locator('.auth-login-dialog'); await dialog.locator('input[placeholder="请输入企业名称"]').fill('platform'); await dialog.locator('input[placeholder="请输入用户名"]').fill('zhangzhen'); await dialog.locator('input[placeholder="请输入密码"]').fill('sz@zz@1116'); await dialog.locator('.login-submit').click(); const composer = page.locator('textarea[placeholder^="输入排产指令"]'); await composer.waitFor({ state: 'visible', timeout: 60_000 }); await page.waitForTimeout(2500); // 柔性排产(让柔性甘特有数据) await composer.fill('跑一版柔性排产'); await composer.press('Enter'); await page.locator('.flex-block-title', { hasText: '柔性排产' }).first() .waitFor({ state: 'visible', timeout: 170_000 }); await page.waitForTimeout(1500); // 打开视口 const resultBtn = page.locator('.right-rail button[data-tip="会话结果"], .right-rail button[data-tip="再点收起"]').first(); if (!(await page.locator('.vp-toolbar').isVisible().catch(() => false))) { try { await resultBtn.click(); } catch {} } await page.locator('.vp-toolbar').waitFor({ state: 'visible', timeout: 20_000 }).catch(() => {}); const tab = async (label, name) => { try { const t = page.locator('.seg .tab', { hasText: label }).first(); await t.scrollIntoViewIfNeeded({ timeout: 8_000 }); await t.click({ timeout: 8_000 }); await page.waitForTimeout(1500); await shot(name); } catch (e) { console.log(' ✗', label, e.message.slice(0, 60)); } }; await tab('柔性甘特', '21-柔性甘特'); await tab('世界对比', '22-世界对比'); await tab('利用率', '23-利用率'); await browser.close(); console.log('补拍完成'); cleanup(); process.exit(0);