const fs = require('fs') const path = require('path') const WebSocket = require('ws') const outputDir = path.resolve(__dirname, 'full-screenshots') const pageFilter = new Set(String(process.env.MES_MANUAL_PAGE_FILTER || '').split('|').filter(Boolean)) const manifestPath = path.join(outputDir, pageFilter.size ? 'manifest-recapture.json' : 'manifest.json') const baseUrl = process.env.MES_MANUAL_BASE_URL || 'https://127.0.0.1:1998' const debugPort = process.env.MES_MANUAL_DEBUG_PORT || '9223' const captureDelay = Number(process.env.MES_MANUAL_CAPTURE_DELAY || 5000) const loginUser = process.env.MES_MANUAL_USER const loginPassword = process.env.MES_MANUAL_PASSWORD const loginRole = process.env.MES_MANUAL_ROLE const loginPersonnelId = process.env.MES_MANUAL_PERSONNEL_ID || '1' const loginName = process.env.MES_MANUAL_PERSONNEL_NAME || '超级管理员' const loginAccount = process.env.MES_MANUAL_ACCOUNT || '0000' fs.mkdirSync(outputDir, { recursive: true }) const modules = [ { key: 'device', title: '设备管理', pages: [ ['设备信息维护', '/DeviceManagement/DeviceInformation/index', 'DeviceManagement/DeviceInformation/index.vue'], ['设备数据', '/DeviceManagement/Devicedata/index', 'DeviceManagement/Devicedata/index.vue'], ['设备历史数据', '/DeviceManagement/DeviceDataHistory/index', 'DeviceManagement/DeviceDataHistory/index.vue'], ['实时数据', '/DeviceManagement/DeviceRealData/index', 'DeviceManagement/DeviceRealData/index.vue'] ] }, { key: 'research', title: '研发管理', pages: [ ['设计任务', '/ResearchManagement/DesignReportTask/Task/index', 'ProductionManagement/DesignReportTask/Task/index.vue'], ['设计报工', '/ResearchManagement/DesignReportTask/Report/index', 'ProductionManagement/DesignReportTask/Report/index.vue'], ['设计工时校验', '/ResearchManagement/DesignReportTask/Check/index', 'ProductionManagement/DesignReportTask/Check/index.vue'], ['设计工时报表', '/ResearchManagement/DesignReportTask/HoursReport/index', 'ProductionManagement/DesignReportTask/HoursReport/index.vue'] ] }, { key: 'process', title: '工艺管理', pages: [ ['工序管理', '/ProcessManagement/ProcedureManagement/index', 'ProcessManagement/ProcedureManagement/index.vue'], ['制定标准工艺', '/ProcessManagement/ProcessesDevelop/index', 'ProcessManagement/ProcessesDevelop/index.vue'], ['生产工艺', '/ProcessManagement/ProcessProduct/index', 'ProcessManagement/ProcessProduct/index.vue'], ['工艺查询', '/ProcessManagement/ProcessInquiry/index', 'ProcessManagement/ProcessInquiry/index.vue'], ['工艺任务', '/ProcessManagement/ProductionOrder/index', 'ProcessManagement/ProductionOrder/index.vue'], ['图纸维护', '/ProcessManagement/PartDrawing/index', 'ProcessManagement/PartDrawing/index.vue'], ['工艺维护', '/ProcessManagement/ProcessDrawing/index', 'ProcessManagement/ProcessDrawing/index.vue'], ['查看图纸', '/ProcessManagement/PartDrawlook/index', 'ProcessManagement/PartDrawlook/index.vue'], ['采购图纸查询', '/ProcessManagement/ProcurementDrawlook/index', 'ProcessManagement/ProcurementDrawlook/index.vue'] ] }, { key: 'plan', title: '计划排产', pages: [ ['机加件排产', '/PlanManagement/SelfMakePlan/index', 'PlanManagement/SelfMakePlan/index.vue'], ['装配件排产', '/PlanManagement/InstallMakePlan/index', 'PlanManagement/InstallMakePlan/index.vue'], ['机加件已排产', '/PlanManagement/SelfMakePlandone/index', 'PlanManagement/SelfMakePlandone/index.vue'], ['机加件未排产', '/PlanManagement/SelfMakePlando/index', 'PlanManagement/SelfMakePlando/index.vue'], ['优先装配', '/PlanManagement/priorityinstall/index', 'PlanManagement/priorityinstall/index.vue'], ['计划用表', '/PlanManagement/PlanShell/index', 'PlanManagement/PlanShell/index.vue'], ['装配未排产', '/PlanManagement/InstallMakePlanDo/index', 'PlanManagement/InstallMakePlanDo/index.vue'], ['生产订单关闭', '/PlanManagement/PlannOrderClose/index', 'PlanManagement/PlannOrderClose/index.vue'], ['工位任务汇总', '/PlanManagement/WorkstationPlan/index', 'PlanManagement/WorkstationPlan/index.vue'], ['装配已排产', '/PlanManagement/InstallMakePlanDone/index', 'PlanManagement/InstallMakePlanDone/index.vue'], ['系统类装配任务', '/PlanManagement/SystemInstallTask/index', 'PlanManagement/SystemInstallTask/index.vue'], ['阀类装配任务', '/PlanManagement/ValueInstallTask/index', 'PlanManagement/ValueInstallTask/index.vue'], ['及时齐套跟踪结果', '/PlanManagement/TimelyKitTrackingResult/index', 'PlanManagement/TimelyKitTrackingResult/index.vue'] ] }, { key: 'production', title: '生产管理', pages: [ ['生产计划跟踪', '/ProductionManagement/ProductionPlanTrack/index', 'ProductionManagement/ProductionPlanTrack/index.vue'], ['加工中心', '/ProductionManagement/MachiningCenter/index', 'ProductionManagement/MachiningCenter/index.vue'], ['过程异常管控', '/ProductionManagement/AbnormalControl/index', 'ProductionManagement/AbnormalControl/index.vue'], ['工时校验', '/ProductionManagement/WorkHoursCheck/index', 'ProductionManagement/WorkHoursCheck/index.vue'], ['外协管理', '/ProductionManagement/OutsourceMange/index', 'ProductionManagement/OutsourceMange/index.vue'], ['人员绑定', '/ProductionManagement/PersonnelBinding/index', 'ProductionManagement/PersonnelBinding/index.vue'], ['装配中心', '/ProductionManagement/AssemblyCenter/index', 'ProductionManagement/AssemblyCenter/index.vue'], ['班次管理', '/ProductionManagement/ShiftManagement/index', 'ProductionManagement/ShiftManagement/index.vue'], ['工时报表', '/ProductionManagement/Timesheet/index', 'ProductionManagement/Timesheet/index.vue'], ['加急件管理', '/ProductionManagement/Urgent/index', 'ProductionManagement/Urgent/index.vue'], ['拣配任务', '/ProductionManagement/PickingTask/index', 'ProductionManagement/PickingTask/index.vue'], ['采购外协管理', '/ProductionManagement/SaleOutsourceMange/index', 'ProductionManagement/SaleOutsourceMange/index.vue'], ['工时查询', '/ProductionManagement/Workhours/index', 'ProductionManagement/Workhours/index.vue'], ['机加正在进行中', '/ProductionManagement/DeviceStatus/index', 'ProductionManagement/DeviceStatus/index.vue'], ['装配状态', '/ProductionManagement/AssembleStatus/index', 'ProductionManagement/AssembleStatus/index.vue'], ['外协报表', '/ProductionManagement/OutsourceSheet/index', 'ProductionManagement/OutsourceSheet/index.vue'], ['工时修正', '/ProductionManagement/WorkhoursEdit/index', 'ProductionManagement/WorkhoursEdit/index.vue'], ['物料机器人', '/ProductionManagement/AGVtask/index', 'ProductionManagement/AGVtask/index.vue'], ['手续确认报表', '/ProductionManagement/FormalityConfirmReport/index', 'ProductionManagement/FormalityConfirmReport/index.vue'], ['发货通知列表', '/ProductionManagement/DeliveryNoticeList/index', 'ProductionManagement/DeliveryNoticeList/index.vue'], ['结算工时', '/ProductionManagement/SettlementWorkhours/index', 'ProductionManagement/SettlementWorkhours/index.vue'] ] }, { key: 'anomaly', title: '异常提醒', pages: [ ['机加及时开工', '/AnomalousManagement/PunctualWork/index', 'AnomalousManagement/PunctualWork/index.vue'], ['外协及时开工', '/AnomalousManagement/OutPunctualWork/index', 'AnomalousManagement/OutPunctualWork/index.vue'], ['加急任务列表', '/AnomalousManagement/UrgentTask/index', 'AnomalousManagement/UrgentTask/index.vue'], ['装配及时送检', '/AnomalousManagement/AssemblyPunctualInspection/index', 'AnomalousManagement/AssemblyPunctualInspection/index.vue'] ] }, { key: 'quality', title: '质量管理', pages: [ ['终检', '/QualityManagement/CheckTask/final', 'QualityManagement/CheckTask/final.vue'], ['采购入库检', '/QualityManagement/CheckTask/purchaseIncoming', 'QualityManagement/CheckTask/purchaseIncoming.vue'], ['销售退货检', '/QualityManagement/CheckTask/salesReturn', 'QualityManagement/CheckTask/salesReturn.vue'], ['原材料退库检', '/QualityManagement/CheckTask/materialReturn', 'QualityManagement/CheckTask/materialReturn.vue'], ['序检收检', '/QualityManagement/CheckTask/processReceive', 'QualityManagement/CheckTask/processReceive.vue'], ['质量检验标准', '/QualityManagement/QualityStandardQuery/index', 'QualityManagement/QualityStandardQuery/index.vue'], ['其他入库检', '/QualityManagement/CheckTask/OtherInbound', 'QualityManagement/CheckTask/OtherInbound.vue'], ['质检明细', '/QualityManagement/QualityDetails/index', 'QualityManagement/QualityDetails/index.vue'], ['不合格品处置', '/QualityManagement/Unqualified/index', 'QualityManagement/Unqualified/index.vue'], ['质检报工', '/QualityManagement/QualityCenter/index', 'QualityManagement/QualityCenter/index.vue'], ['测试装配任务', '/QualityManagement/TestAssemblyTask/index', 'QualityManagement/TestAssemblyTask/index.vue'], ['及时检验报表', '/QualityManagement/CheckTask/punctualInspection', 'QualityManagement/CheckTask/punctualInspection.vue'], ['产品合格率(工位)', '/QualityManagement/ProductPassRateWorkstation/index', 'ProductionManagement/ProductPassRateWorkstation/index.vue'] ] }, { key: 'sales', title: '销售订单管理', pages: [ ['销售订单状态', '/SaleManagement/saleorderstatus/index', 'SaleManagement/saleorderstatus/index.vue'] ] } ] const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) async function connect() { const targets = await fetch(`http://127.0.0.1:${debugPort}/json`).then(res => res.json()) const target = targets.find(item => item.type === 'page') if (!target) throw new Error('No Chrome page target found') const ws = new WebSocket(target.webSocketDebuggerUrl) let id = 0 const pending = new Map() ws.on('message', raw => { const message = JSON.parse(raw.toString()) if (message.id && pending.has(message.id)) { const request = pending.get(message.id) pending.delete(message.id) message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result) } }) await new Promise((resolve, reject) => { ws.once('open', resolve) ws.once('error', reject) }) const send = (method, params = {}) => new Promise((resolve, reject) => { const requestId = ++id pending.set(requestId, { resolve, reject }) ws.send(JSON.stringify({ id: requestId, method, params })) }) return { ws, send } } async function main() { const { ws, send } = await connect() await send('Page.enable') await send('Runtime.enable') await send('Network.enable') await send('Security.enable') await send('Security.setIgnoreCertificateErrors', { ignore: true }) await send('Emulation.setDeviceMetricsOverride', { width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false }) const evaluate = expression => send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }).then(result => result.result.value) async function waitFor(expression, timeout = 45000) { const started = Date.now() while (Date.now() - started < timeout) { if (await evaluate(`Boolean(${expression})`)) return true await wait(300) } return false } async function clickByText(text, selector = 'button') { return evaluate(`(() => { const normalize = value => String(value || '').replace(/\\s+/g, '') const node = [...document.querySelectorAll(${JSON.stringify(selector)})] .find(item => item.offsetParent !== null && !item.disabled && normalize(item.innerText).includes(${JSON.stringify(text.replace(/\s+/g, ''))})) if (!node) return false node.click() return true })()`) } async function login() { if (loginRole) { for (const [name, value] of [ ['Admin-Token', loginRole], ['id', loginPersonnelId], ['name', loginName], ['account', loginAccount] ]) { await send('Network.setCookie', { name, value, url: baseUrl }) } await send('Page.navigate', { url: `${baseUrl}/#/` }) const ready = await waitFor("document.querySelector('.sidebar-container')", 60000) if (!ready) throw new Error('Cookie session did not reach the application shell') return } await send('Page.navigate', { url: `${baseUrl}/#/login` }) await waitFor("document.querySelector('input[name=username]') || document.querySelector('.sidebar-container')") if (await evaluate("Boolean(document.querySelector('input[name=username]'))")) { if (!loginUser || !loginPassword) throw new Error('MES manual login credentials are not available') await evaluate(`(() => { const setValue = (element, value) => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set setter.call(element, value) element.dispatchEvent(new Event('input', { bubbles: true })) element.dispatchEvent(new Event('change', { bubbles: true })) } setValue(document.querySelector('input[name=username]'), ${JSON.stringify(loginUser)}) setValue(document.querySelector('input[name=password]'), ${JSON.stringify(loginPassword)}) })()`) await clickByText('登录') await wait(1200) if (await evaluate("Boolean(document.querySelector('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__row'))")) { await evaluate("document.querySelector('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__row').click()") await clickByText('确定', '.el-dialog__wrapper:not([style*="display: none"]) button') } } const ready = await waitFor("document.querySelector('.sidebar-container')", 60000) if (!ready) throw new Error('Login did not reach the application shell') } async function capture(page, fileName) { await send('Page.navigate', { url: `${baseUrl}/#${page.route}` }) const ready = await waitFor("document.readyState === 'complete' && (document.querySelector('.app-main') || document.querySelector('.app-container'))") if (!ready) throw new Error(`Page shell timed out: ${page.route}`) await waitFor("!document.querySelector('.el-loading-mask:not([style*=\"display: none\"])')", 15000) if (page.title === '生产工艺') { await waitFor(`(() => { const app = document.querySelector('#app') && document.querySelector('#app').__vue__ return Boolean(app && app.$route && app.$route.matched && app.$route.matched .some(record => Object.values(record.instances || {}).some(instance => instance && typeof instance.searchTable === 'function'))) })()`, 15000) const filtered = await evaluate(`(() => { const app = document.querySelector('#app') && document.querySelector('#app').__vue__ const records = app && app.$route && app.$route.matched ? [...app.$route.matched].reverse() : [] for (const record of records) { const instances = Object.values(record.instances || {}).filter(Boolean) const vm = instances.find(instance => instance.taskstatus !== undefined && typeof instance.searchTable === 'function') if (!vm) continue vm.taskstatus = '2' vm.searchTable() return true } return false })()`) if (!filtered) throw new Error('生产工艺页面路由实例未就绪') } else { await clickByText('查询') } await waitFor("!document.querySelector('.el-loading-mask:not([style*=\"display: none\"])')", 30000) if (page.title === '机加件已排产') { await waitFor("document.querySelectorAll('.el-table__body-wrapper .el-table__row').length > 0", 60000) } await wait(captureDelay) await evaluate('window.scrollTo(0, 0)') const result = await send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false, fromSurface: true }) fs.writeFileSync(path.join(outputDir, fileName), Buffer.from(result.data, 'base64')) return evaluate(`({ title: document.title, bodyTextLength: document.body ? document.body.innerText.length : 0, hasError: Boolean(document.querySelector('.el-message--error')), route: location.hash })`) } async function captureEditDialog(page) { const dialogConfig = { '机加件已排产': { button: '编辑', title: '编辑备注信息', fileName: '04-03-plan-edit-dialog.png' }, '生产工艺': { button: '编辑工艺', title: '编辑生产工艺', fileName: '03-03-process-edit-dialog.png' } }[page.title] if (!dialogConfig) return null let opened = await evaluate(`(() => { const normalize = value => String(value || '').replace(/\\s+/g, '') const button = [...document.querySelectorAll('button')] .find(item => normalize(item.innerText) === '${dialogConfig.button}') if (!button) return false button.click() return true })()`) if (!opened) throw new Error(`${page.title}页面未找到可用的${dialogConfig.button}按钮`) const dialogReady = await waitFor(`[...document.querySelectorAll('.el-dialog__wrapper:not([style*="display: none"])')] .some(item => String(item.innerText || '').includes('${dialogConfig.title}'))`, 15000) if (!dialogReady) throw new Error(`${dialogConfig.title}弹窗未打开`) if (page.title === '生产工艺') { let populated = await waitFor("document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__body-wrapper .el-table__row').length > 0", 5000) const buttonCount = await evaluate(`(() => { const normalize = value => String(value || '').replace(/\\s+/g, '') return [...document.querySelectorAll('button')] .filter(item => item.offsetParent !== null && normalize(item.innerText) === '${dialogConfig.button}').length })()`) for (let index = 1; !populated && index < buttonCount; index += 1) { await evaluate(`(() => { const dialog = [...document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"])')] .find(item => String(item.innerText || '').includes('${dialogConfig.title}')) const close = dialog && dialog.querySelector('.el-dialog__headerbtn') if (close) close.click() })()`) await waitFor(`![...document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"])')] .some(item => String(item.innerText || '').includes('${dialogConfig.title}'))`, 5000) opened = await evaluate(`(() => { const normalize = value => String(value || '').replace(/\\s+/g, '') const buttons = [...document.querySelectorAll('button')] .filter(item => item.offsetParent !== null && normalize(item.innerText) === '${dialogConfig.button}') if (!buttons[${index}]) return false buttons[${index}].click() return true })()`) if (!opened) continue await waitFor(`[...document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"])')] .some(item => String(item.innerText || '').includes('${dialogConfig.title}'))`, 5000) populated = await waitFor("document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__body-wrapper .el-table__row').length > 0", 5000) } } await wait(1000) const result = await send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false, fromSurface: true }) fs.writeFileSync(path.join(outputDir, dialogConfig.fileName), Buffer.from(result.data, 'base64')) return { fileName: dialogConfig.fileName, title: dialogConfig.title } } await login() const manifest = [] const totalPages = modules.reduce((total, module) => { return total + module.pages.filter(page => pageFilter.size === 0 || pageFilter.has(page[0])).length }, 0) let sequence = 0 for (let moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) { const module = modules[moduleIndex] for (let pageIndex = 0; pageIndex < module.pages.length; pageIndex++) { sequence++ const [title, route, source] = module.pages[pageIndex] if (pageFilter.size && !pageFilter.has(title)) { sequence-- continue } const fileName = `${String(moduleIndex + 1).padStart(2, '0')}-${String(pageIndex + 1).padStart(2, '0')}-${module.key}.png` const item = { sequence, module: module.title, title, route, source, fileName, status: 'pending' } try { item.pageState = await capture(item, fileName) item.dialogCapture = await captureEditDialog(item) item.status = 'captured' console.log(`[${sequence}/${totalPages}] ${module.title} / ${title}`) } catch (error) { item.status = 'failed' item.error = error.message console.error(`[${sequence}/${totalPages}] FAILED ${module.title} / ${title}: ${error.message}`) } manifest.push(item) fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8') } } ws.close() const failed = manifest.filter(item => item.status !== 'captured') console.log(`Captured ${manifest.length - failed.length}/${manifest.length} pages`) await send('Browser.close') if (failed.length) process.exitCode = 1 } module.exports = { modules } if (require.main === module) { main().catch(error => { console.error(error.stack || error.message) process.exitCode = 1 }) }