Files
MES_Manage_View_V20/work/system-manual/capture-full-manual-pages.js

413 lines
22 KiB
JavaScript

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
})
}