799 lines
54 KiB
JavaScript
799 lines
54 KiB
JavaScript
const fs = require('fs')
|
||
const path = require('path')
|
||
const JSZip = require('jszip')
|
||
const { DOMParser, XMLSerializer } = require('@xmldom/xmldom')
|
||
|
||
const root = path.resolve(__dirname, '..', '..')
|
||
const docxPath = path.resolve(root, 'doc', '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23.docx')
|
||
const backupDir = path.resolve(__dirname, 'backups')
|
||
const backupPath = path.resolve(backupDir, '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23_修改前备份.docx')
|
||
const stagingPath = path.resolve(__dirname, 'revised-manual.docx')
|
||
const catalog = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'full-page-catalog.json'), 'utf8'))
|
||
const dialogAudit = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'manual-dialog-audit.json'), 'utf8'))
|
||
const actionAudit = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'manual-action-audit.json'), 'utf8'))
|
||
const screenshotsDir = path.resolve(__dirname, 'full-screenshots')
|
||
|
||
const NS = {
|
||
w: 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
|
||
r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
||
a: 'http://schemas.openxmlformats.org/drawingml/2006/main',
|
||
rel: 'http://schemas.openxmlformats.org/package/2006/relationships',
|
||
xml: 'http://www.w3.org/XML/1998/namespace'
|
||
}
|
||
|
||
function localName(node) {
|
||
return node && (node.localName || String(node.nodeName || '').split(':').pop())
|
||
}
|
||
|
||
function elementChildren(node, name) {
|
||
if (!node || !node.childNodes) return []
|
||
return [...node.childNodes].filter(child => child.nodeType === 1 && (!name || localName(child) === name))
|
||
}
|
||
|
||
function descendants(node, name, output = []) {
|
||
for (const child of elementChildren(node)) {
|
||
if (!name || localName(child) === name) output.push(child)
|
||
descendants(child, name, output)
|
||
}
|
||
return output
|
||
}
|
||
|
||
function textOf(node) {
|
||
return descendants(node, 't').map(item => item.textContent || '').join('').trim()
|
||
}
|
||
|
||
function firstDescendant(node, name) {
|
||
return descendants(node, name, [])[0] || null
|
||
}
|
||
|
||
function createElement(document, name) {
|
||
return document.createElementNS(NS.w, `w:${name}`)
|
||
}
|
||
|
||
function setParagraphText(document, paragraph, text) {
|
||
const pPr = elementChildren(paragraph, 'pPr')[0] || null
|
||
const sourceRPr = firstDescendant(paragraph, 'rPr')
|
||
for (const child of [...paragraph.childNodes]) {
|
||
if (child !== pPr) paragraph.removeChild(child)
|
||
}
|
||
const run = createElement(document, 'r')
|
||
if (sourceRPr) run.appendChild(sourceRPr.cloneNode(true))
|
||
const textNode = createElement(document, 't')
|
||
if (/^\s|\s$/.test(text)) textNode.setAttributeNS(NS.xml, 'xml:space', 'preserve')
|
||
textNode.appendChild(document.createTextNode(text))
|
||
run.appendChild(textNode)
|
||
paragraph.appendChild(run)
|
||
return paragraph
|
||
}
|
||
|
||
function setCellText(document, cell, text) {
|
||
let paragraph = elementChildren(cell, 'p')[0]
|
||
if (!paragraph) {
|
||
paragraph = createElement(document, 'p')
|
||
cell.appendChild(paragraph)
|
||
}
|
||
setParagraphText(document, paragraph, text)
|
||
for (const extra of elementChildren(cell, 'p').slice(1)) cell.removeChild(extra)
|
||
}
|
||
|
||
function cellTexts(row) {
|
||
return elementChildren(row, 'tc').map(cell => textOf(cell))
|
||
}
|
||
|
||
function setRowValues(document, row, values) {
|
||
const cells = elementChildren(row, 'tc')
|
||
for (let index = 0; index < cells.length; index++) {
|
||
setCellText(document, cells[index], values[index] || '')
|
||
}
|
||
}
|
||
|
||
function makeParagraph(document, template, text) {
|
||
return setParagraphText(document, template.cloneNode(true), text)
|
||
}
|
||
|
||
function makeTable(document, template, rows) {
|
||
const table = template.cloneNode(true)
|
||
const sourceRows = elementChildren(table, 'tr')
|
||
if (!sourceRows.length) throw new Error('Table template has no rows')
|
||
const headerTemplate = sourceRows[0].cloneNode(true)
|
||
const dataTemplate = (sourceRows[1] || sourceRows[0]).cloneNode(true)
|
||
for (const row of sourceRows) table.removeChild(row)
|
||
rows.forEach((values, index) => {
|
||
const row = (index === 0 ? headerTemplate : dataTemplate).cloneNode(true)
|
||
setRowValues(document, row, values)
|
||
table.appendChild(row)
|
||
})
|
||
return table
|
||
}
|
||
|
||
function findDirectParagraphIndex(bodyChildren, exactText) {
|
||
return bodyChildren.findIndex(node => localName(node) === 'p' && textOf(node) === exactText)
|
||
}
|
||
|
||
function replaceAllTextNodes(document, replacements) {
|
||
for (const textNode of descendants(document.documentElement, 't')) {
|
||
let value = textNode.textContent || ''
|
||
for (const [search, replacement] of replacements) value = value.split(search).join(replacement)
|
||
textNode.textContent = value
|
||
}
|
||
}
|
||
|
||
function summarize(values, limit = 18) {
|
||
const unique = [...new Set(values.map(value => String(value || '').trim()).filter(Boolean))]
|
||
if (!unique.length) return '按弹窗提示查看或填写相关业务内容'
|
||
return unique.slice(0, limit).join('、') + (unique.length > limit ? `等${unique.length}项` : '')
|
||
}
|
||
|
||
function normalizeLabel(value) {
|
||
return String(value || '').replace(/\s+/g, '').replace(/[::]/g, '')
|
||
}
|
||
|
||
function dialogInfo(page) {
|
||
const item = dialogAudit.find(entry => entry.module === page.module && entry.title === page.title)
|
||
return item && Array.isArray(item.dialogs) ? item.dialogs : []
|
||
}
|
||
|
||
function dialogTitleForAction(page, action) {
|
||
const method = String(action.method || '').toLowerCase()
|
||
const expression = String(action.expression || '').toLowerCase()
|
||
const title = String(page.title || '')
|
||
const rules = [
|
||
[/submitreceivecheckdata/, '收检'],
|
||
[/sendinspection/, '送检'],
|
||
[/abnormalform|addworktimerecord/, '异常工时'],
|
||
[/submitaddrecord/, '新增工时记录'],
|
||
[/submiteditform|edittask/, '编辑任务信息'],
|
||
[/submitreturnware|returnwarehouse/, '退库单'],
|
||
[/submitmaterialrequisition|materialrequisition/, '领料单'],
|
||
[/workendform|workendvisible/, '报工结束'],
|
||
[/confirmovertime|overtimedialog/, '加班确认'],
|
||
[/submitremark|remarkdialog/, '编辑备注信息'],
|
||
[/submitcopy|dialogformvisible/, '生产工艺拷贝'],
|
||
[/processsave|saveprocess|workprocess|downone|upone|insertone|deleteone/, '编辑生产工艺'],
|
||
[/assistendvisible/, '收检'],
|
||
[/editdialogvisible/, '编辑任务信息'],
|
||
[/returnwarehousepop/, '退库单'],
|
||
[/materialrequisitionpop/, '领料单'],
|
||
[/misspop/, '缺料单'],
|
||
[/seevisible/, '图纸'],
|
||
[/urgentvisible/, '加急明细']
|
||
]
|
||
for (const [pattern, mappedTitle] of rules) {
|
||
if (pattern.test(method) || pattern.test(expression)) return mappedTitle
|
||
}
|
||
if (title === '工序管理' && (method === 'editprocess' || expression.includes('editprocess'))) return '业务操作'
|
||
if (title === '机加件已排产' && (method === 'editsum' || method === 'editremark')) return '编辑备注信息'
|
||
const dialogs = dialogInfo(page)
|
||
if (dialogs.length === 1) return dialogs[0].title || `${title}操作窗口`
|
||
return dialogs[0] ? (dialogs[0].title || `${title}操作窗口`) : `${title}确认窗口`
|
||
}
|
||
|
||
function dialogFieldsForAction(page, action) {
|
||
const mappedTitle = dialogTitleForAction(page, action)
|
||
const dialog = dialogInfo(page).find(item => item.title === mappedTitle)
|
||
if (!dialog) return ''
|
||
return summarize([...(dialog.fields || []), ...(dialog.columns || [])], 8)
|
||
}
|
||
|
||
function friendlyActionLabel(page, action) {
|
||
const raw = normalizeLabel(action.label)
|
||
const method = String(action.method || '').toLowerCase()
|
||
const expression = String(action.expression || '').toLowerCase()
|
||
const dialogTitle = dialogTitleForAction(page, action)
|
||
if (method === 'receivecheck') {
|
||
if (page.title === '原材料退库检') return '退库检'
|
||
if (page.title === '序检收检') return '收检'
|
||
return '入库检'
|
||
}
|
||
if (method === 'finalcheck') return '终检记录'
|
||
if (method === 'change' && page.title === '加急件管理') return '加急状态调整'
|
||
if (method === 'betweenprocessprevpage') return '序检自检窗口的上/下条、保存与删除'
|
||
if (raw.includes('生产工艺') && raw.includes('取消')) return '生产工艺拷贝弹窗【取消】'
|
||
if (method.includes('search') || method.includes('query') || /查询/.test(raw) || /status-light|\?|</.test(raw)) return '查询'
|
||
if (/检验详情/.test(raw)) return '查看检验详情'
|
||
if (/上一个下一个保存当前记录删除当前记录关闭/.test(raw)) return `${dialogTitle}窗口导航、保存与删除`
|
||
if (/删除当前记录关闭/.test(raw)) return `${dialogTitle}窗口删除当前记录并关闭`
|
||
if (/确定关闭/.test(raw)) return `${dialogTitle}弹窗【确定】`
|
||
if (/取消确定/.test(raw)) return `${dialogTitle}弹窗取消或确定`
|
||
if (/^(确定|确 定|确认)$/.test(String(action.label || '').trim()) || raw === '确定' || raw === '确认') return `${dialogTitle}弹窗【确定】`
|
||
if (/^(取消|取 消)$/.test(String(action.label || '').trim()) || raw === '取消') return `${dialogTitle}弹窗【取消】`
|
||
if (raw === '关闭') return `${dialogTitle}窗口【关闭】`
|
||
if (/查询/.test(raw) && /0|status/.test(raw)) return '查询'
|
||
if (/多余|status-light|scope\.row|optor|parameter|visible|form|process|savecurrent|submit/.test(raw)) {
|
||
if (/编辑|edit/.test(method + expression)) return `当前记录【编辑】`
|
||
return `当前页面业务操作`
|
||
}
|
||
return raw || `当前页面业务操作`
|
||
}
|
||
|
||
function actionEntry(page, action, label) {
|
||
const dialogTitle = dialogTitleForAction(page, action)
|
||
const fields = dialogFieldsForAction(page, action)
|
||
if (label === '查询') return `在“${page.title}”页面顶部筛选区填写或选择条件,点击【查询】。`
|
||
if (/弹窗【取消】$/.test(label)) return `在“${dialogTitle}”弹窗核对不需要提交本次修改后,点击【取消】关闭该弹窗。`
|
||
if (/弹窗【确定】$/.test(label)) return `在“${dialogTitle}”弹窗填写或核对${fields || '当前内容'},点击【确定】提交本次弹窗内容。`
|
||
if (/弹窗取消或确定$/.test(label)) return `在“${dialogTitle}”弹窗填写或核对内容;需要提交点击【确定】,放弃本次修改点击【取消】。`
|
||
if (/窗口【关闭】$/.test(label)) return `在“${dialogTitle}”查看窗口核对内容后,点击【关闭】返回当前列表。`
|
||
if (/编辑/.test(label)) return `在“${page.title}”当前目标记录的行操作区点击【编辑】,打开${dialogTitle}窗口。`
|
||
if (/新增|添加/.test(label)) return `在“${page.title}”页面点击【${label.replace(/^.*?【|】.*$/g, '') || '新增'}】,填写弹窗中的必填项后按提示确认。`
|
||
if (/删除|移除/.test(label)) return `在“${page.title}”列表定位目标记录,点击对应【${label.replace(/^.*?【|】.*$/g, '') || '删除'}】,在确认提示中核对对象后再确认。`
|
||
if (/保存/.test(label)) return `在“${page.title}”完成当前记录或窗口内容修改后,点击对应【保存】并等待页面提示。`
|
||
if (/上移|下移|插入|下发|派工|开始|结束|报工|送检|收检|入库|退库|上传|导出|打印|查看|图纸|日程|表单/.test(label)) return `在“${page.title}”定位目标记录或任务,点击【${label.replace(/.*?【|】.*/g, '') || label}】,按页面提示完成后续步骤。`
|
||
return `在“${page.title}”定位目标记录后,点击【${label}】,根据页面提示完成操作。`
|
||
}
|
||
|
||
function downstreamImpact(page) {
|
||
const relation = String(page.moduleRelation || '').split(';').map(value => value.trim()).filter(Boolean)
|
||
return relation[1] || relation[0] || `后续业务以${page.title}页面保存的最新结果为准。`
|
||
}
|
||
|
||
function actionParameters(action) {
|
||
const ignored = /^(PageCurrent|PageSize|PageCount|ItemCount|分页|每页|返回值|result)$/i
|
||
const values = [...new Set((action.parameters || [])
|
||
.map(value => String(value || '').trim())
|
||
.filter(value => value && !ignored.test(value)))]
|
||
return values.slice(0, 6).join('、') + (values.length > 6 ? `等${values.length}项` : '')
|
||
}
|
||
|
||
function hasWriteProcedure(page, action) {
|
||
const writeProcedures = new Set(page.writeProcedures || [])
|
||
return (action.procedures || []).some(name => writeProcedures.has(name))
|
||
}
|
||
|
||
function isWriteAction(page, action) {
|
||
if (hasWriteProcedure(page, action)) return true
|
||
const label = normalizeLabel(action.label)
|
||
return /新增|添加|保存|确定|提交|编辑|修改|删除|下发|派工|开工|开始|结束|完成|报工|送检|收检|退库|入库|出库|上传|导入|复制|生成|绑定|调整|插入|上移|下移|审核|审批|关闭订单|恢复/.test(label)
|
||
}
|
||
|
||
function effectForAction(page, action) {
|
||
const label = friendlyActionLabel(page, action)
|
||
const rawLabel = normalizeLabel(action.label)
|
||
const parameters = actionParameters(action)
|
||
const writes = isWriteAction(page, action)
|
||
const downstream = downstreamImpact(page)
|
||
const dialogTitle = dialogTitleForAction(page, action)
|
||
const fields = dialogFieldsForAction(page, action)
|
||
if (/弹窗【取消】/.test(label)) return [`关闭“${dialogTitle}”弹窗,不提交本次${fields || '弹窗'}修改。`, '本次弹窗中尚未提交的内容被放弃;此前已经即时保存的表格修改不受影响。']
|
||
if (/窗口【关闭】/.test(label)) return [`关闭“${dialogTitle}”窗口并返回当前页面。`, '仅退出查看窗口,不改变已保存的业务记录。']
|
||
if (/弹窗取消或确定/.test(label)) return [`根据选择执行“${dialogTitle}”弹窗的提交或退出。`, '点击【确定】会提交本次弹窗内容;点击【取消】会放弃本次弹窗修改。']
|
||
if (/弹窗【确定】/.test(label)) return [`提交“${dialogTitle}”弹窗中的${fields || '已填写内容'},成功后关闭弹窗或刷新列表。`, `本次提交结果成为${page.title}后续处理依据;提交前应核对目标记录和字段。${downstream}`]
|
||
if (/查询|搜索|筛选|刷新/.test(label)) return ['按当前筛选条件重新读取并显示匹配记录。', '只刷新页面结果,不修改订单、任务或主数据。']
|
||
if (/导出|下载/.test(label)) return ['按当前筛选范围生成并下载文件。', '不修改系统业务数据;导出内容以下载时的页面结果为准。']
|
||
if (/打印|预览/.test(label)) return ['打开当前记录的预览或打印内容。', '不修改业务数据;打印前应核对记录和版本。']
|
||
if (/查看|详情|图纸|记录|日志|进度|缺料/.test(label) && !writes) return ['打开当前记录的明细、附件或关联信息供查看。', '属于只读查看,不改变当前记录状态。']
|
||
if (/^当前记录【编辑】|^编辑|^修改/.test(label) && !hasWriteProcedure(page, action)) return ['打开当前记录的编辑窗口,并读取已保存内容。', '仅打开编辑窗口不会修改数据;后续是否生效以窗口内的确认或即时保存说明为准。']
|
||
if (/批量生成|生成/.test(label)) return [`根据所选记录和现有基础数据生成${page.title}相关业务记录。`, `生成结果成为后续处理依据;重复生成前应先核对当前状态。${downstream}`]
|
||
if (/复制|拷贝/.test(label)) return ['将选定来源记录复制到当前目标,形成一份可继续维护的数据。', `复制后目标记录与来源记录分别维护;后续使用目标记录的最新内容。${downstream}`]
|
||
if (/新增|添加/.test(label)) return [`打开新增入口;确认后创建${parameters || '当前业务'}记录。`, `新记录成功后进入本页业务范围,并可被后续环节使用。${downstream}`]
|
||
if (/删除/.test(label)) return ['删除当前选定记录,并刷新相关列表或顺序。', `被删除内容不再作为后续处理依据;操作前应确认没有仍需使用的关联任务。${downstream}`]
|
||
if (/上移/.test(label)) return ['将当前记录与上一条记录交换顺序。', `新的顺序立即用于后续执行;调整后应重新核对完整顺序。${downstream}`]
|
||
if (/下移/.test(label)) return ['将当前记录与下一条记录交换顺序。', `新的顺序立即用于后续执行;调整后应重新核对完整顺序。${downstream}`]
|
||
if (/插入/.test(label)) return ['在当前位置增加一条待填写记录或工序位置。', `插入会改变后续记录顺序,完成内容选择后应重新核对并保存当前业务。${downstream}`]
|
||
if (/下发|派工/.test(label)) return ['按当前记录生成或更新可执行任务,并将任务置为下游岗位可处理的状态。', `下发后生产、质量或执行岗位会据此接收任务;再次下发前应核对是否已存在任务。${downstream}`]
|
||
if (/开工|开始/.test(label)) return ['记录当前任务的实际开始时间,并将任务转入进行中状态。', `开始时间进入工时和进度计算;开始后应在实际结束时及时完成报工。${downstream}`]
|
||
if (/结束|完工|完成|报工/.test(label)) return ['记录本次完成数量、时间或结果,并更新任务完成状态。', `结果进入进度、工时、质量或后续流转判断;确认前应核对数量和结果。${downstream}`]
|
||
if (/送检/.test(label)) return ['将当前完成内容转入检验环节,并形成待检记录。', `质量岗位会收到待检任务,检验结果将影响合格数量和后续入库或流转。${downstream}`]
|
||
if (/收检|检验|判定/.test(label)) return ['记录检验接收、实测值或判定结果,并更新检验状态。', `合格与不合格结果会影响生产完成、处置和入库数据。${downstream}`]
|
||
if (/上传|导入/.test(label)) return ['读取所选文件并保存附件或批量业务数据。', `导入或上传后的内容可被本页及关联环节使用;执行前应核对模板、记录范围和文件版本。${downstream}`]
|
||
if (/保存|确定|提交|编辑|修改|调整|设置|绑定/.test(label) || writes) {
|
||
return [`保存当前记录的${parameters || '已填写内容'};成功后页面显示最新结果。`, `修改结果立即成为后续业务依据;操作前应核对目标记录和字段。${downstream}`]
|
||
}
|
||
return [`执行【${label}】并显示对应结果。`, `该操作针对${page.title}当前记录;如随后出现确认按钮,应核对目标记录和弹窗标题后再决定是否提交。${rawLabel && rawLabel !== label ? '文档已将页面内部展示片段转换为用户可识别的操作名称。' : ''}`]
|
||
}
|
||
|
||
function pageActions(page) {
|
||
const audit = actionAudit.find(item => item.module === page.module && item.title === page.title)
|
||
if (!audit) return []
|
||
const preferredLabels = {
|
||
downOne: '下移工序',
|
||
upOne: '上移工序',
|
||
insertOne: '插入工序',
|
||
deleteOne: '删除工序'
|
||
}
|
||
const selected = new Map()
|
||
for (const action of audit.actions || []) {
|
||
const label = normalizeLabel(action.label)
|
||
if (!label || /^小时/.test(label)) continue
|
||
const combinedMaintenance = label.includes('查询') && label.includes('新增标准') && label.includes('修改') && label.includes('删除')
|
||
const expanded = combinedMaintenance
|
||
? ['查询', '新增标准', '修改', '删除', '确定', '取消'].map(command => ({
|
||
...action,
|
||
label: command,
|
||
method: `${action.method || '页面操作'}:${command}`,
|
||
expression: `${action.expression || '页面操作'}:${command}`
|
||
}))
|
||
: [action]
|
||
for (const expandedAction of expanded) {
|
||
const expandedLabel = normalizeLabel(expandedAction.label)
|
||
if (expandedLabel === '点击列表行' && !isWriteAction(page, expandedAction)) continue
|
||
const key = expandedAction.method || expandedAction.expression || expandedLabel
|
||
const existing = selected.get(key)
|
||
const preferred = preferredLabels[expandedAction.method]
|
||
if (!existing || (preferred && expandedLabel === preferred) || (!preferred && expandedLabel.length > normalizeLabel(existing.label).length)) {
|
||
selected.set(key, { ...expandedAction, label: preferred || expandedAction.label })
|
||
}
|
||
}
|
||
}
|
||
return [...selected.values()]
|
||
}
|
||
|
||
function actionRows(page) {
|
||
const actions = pageActions(page)
|
||
if (!actions.length) return [['操作入口', '执行结果', '业务影响与注意事项'], ['页面操作', `按页面提示完成${page.title}查询或维护。`, downstreamImpact(page)]]
|
||
return [
|
||
['操作入口', '执行结果', '业务影响与注意事项'],
|
||
...actions.map(action => {
|
||
const label = friendlyActionLabel(page, action)
|
||
return [actionEntry(page, action, label), ...effectForAction(page, action)]
|
||
})
|
||
]
|
||
}
|
||
|
||
function splitDialogCommands(command) {
|
||
const value = String(command || '').replace(/\s+/g, '')
|
||
const known = ['上一个', '下一个', '保存当前记录', '删除当前记录', '删除', '保存', '确定', '取消', '取 消', '确 定', '关闭', '新增异常工时', '编辑', '查询', '新增标准', '修改']
|
||
const parts = []
|
||
let remaining = value
|
||
while (remaining) {
|
||
const match = known.sort((a, b) => b.length - a.length).find(item => remaining.startsWith(item))
|
||
if (!match) return [command]
|
||
parts.push(match.replace(/\s+/g, ''))
|
||
remaining = remaining.slice(match.length)
|
||
}
|
||
return parts
|
||
}
|
||
|
||
function dialogRows(item, page) {
|
||
const actions = pageActions(page)
|
||
const merged = new Map()
|
||
for (const dialog of item.dialogs) {
|
||
const title = dialog.title || '业务操作弹窗'
|
||
if (!merged.has(title)) merged.set(title, { fields: [], columns: [], commands: [] })
|
||
const target = merged.get(title)
|
||
target.fields.push(...(dialog.fields || []))
|
||
target.columns.push(...(dialog.columns || []))
|
||
target.commands.push(...(dialog.commands || []))
|
||
}
|
||
return [...merged.entries()].map(([title, details]) => {
|
||
const content = summarize([...details.fields, ...details.columns])
|
||
const commands = [...new Set(details.commands.filter(Boolean))]
|
||
const usage = commands.length
|
||
? commands.flatMap(command => splitDialogCommands(command)).map(command => {
|
||
const match = actions.find(action => normalizeLabel(action.label) === normalizeLabel(command))
|
||
if (/取消/.test(command)) return `在“${title}”弹窗点击【取消】:关闭弹窗并放弃本次未提交修改。`
|
||
if (/关闭/.test(command)) return `在“${title}”窗口点击【关闭】:退出查看窗口,不改变已保存内容。`
|
||
if (match) return `在“${title}”弹窗点击【${command}】:${effectForAction(page, match)[0]}`
|
||
if (/确定|保存/.test(command)) return `在“${title}”弹窗点击【${command}】:提交本弹窗中与${title}相关的已填写内容。`
|
||
return `在“${title}”窗口执行【${command}】:完成该窗口对应的操作。`
|
||
}).join(' ')
|
||
: '该窗口用于查看当前记录的关联内容,关闭窗口不会修改业务数据。'
|
||
return [title, content, usage]
|
||
})
|
||
}
|
||
|
||
async function main() {
|
||
if (!fs.existsSync(docxPath)) throw new Error(`Manual not found: ${docxPath}`)
|
||
fs.mkdirSync(backupDir, { recursive: true })
|
||
if (!fs.existsSync(backupPath)) fs.copyFileSync(docxPath, backupPath)
|
||
|
||
// Always regenerate from the untouched backup so repeated runs stay idempotent.
|
||
const sourcePath = fs.existsSync(backupPath) ? backupPath : docxPath
|
||
const zip = new JSZip(fs.readFileSync(sourcePath))
|
||
const parser = new DOMParser()
|
||
const serializer = new XMLSerializer()
|
||
const document = parser.parseFromString(zip.file('word/document.xml').asText(), 'application/xml')
|
||
const relationships = parser.parseFromString(zip.file('word/_rels/document.xml.rels').asText(), 'application/xml')
|
||
const body = firstDescendant(document.documentElement, 'body')
|
||
if (!body) throw new Error('Word document body not found')
|
||
|
||
const relationshipMap = new Map()
|
||
let maxRelationshipId = 0
|
||
for (const relation of elementChildren(relationships.documentElement, 'Relationship')) {
|
||
const id = relation.getAttribute('Id')
|
||
relationshipMap.set(id, relation)
|
||
const numeric = Number(String(id).replace(/^rId/, ''))
|
||
if (Number.isFinite(numeric)) maxRelationshipId = Math.max(maxRelationshipId, numeric)
|
||
}
|
||
|
||
const chapterByModule = {
|
||
'设备管理': 7,
|
||
'研发管理': 8,
|
||
'工艺管理': 9,
|
||
'计划排产': 10,
|
||
'生产管理': 11,
|
||
'异常提醒': 12,
|
||
'质量管理': 13,
|
||
'销售订单管理': 14
|
||
}
|
||
|
||
let replacedScreenshots = 0
|
||
for (const page of catalog) {
|
||
const chapter = chapterByModule[page.module]
|
||
const heading = `${chapter}.${page.pageIndex} ${page.title}`
|
||
const bodyChildren = elementChildren(body)
|
||
const start = findDirectParagraphIndex(bodyChildren, heading)
|
||
if (start < 0) throw new Error(`Page heading not found: ${heading}`)
|
||
const nextPagePrefix = `${chapter}.${page.pageIndex + 1} `
|
||
let end = bodyChildren.findIndex((node, index) => index > start && localName(node) === 'p' && textOf(node).startsWith(nextPagePrefix))
|
||
if (end < 0) end = bodyChildren.findIndex((node, index) => index > start && localName(node) === 'p' && /^\d+ /.test(textOf(node)))
|
||
if (end < 0) end = bodyChildren.length
|
||
const imageParagraph = bodyChildren.slice(start, end).find(node => localName(node) === 'p' && firstDescendant(node, 'blip'))
|
||
if (!imageParagraph) throw new Error(`Screenshot drawing not found: ${heading}`)
|
||
const blip = firstDescendant(imageParagraph, 'blip')
|
||
const relationshipId = blip.getAttributeNS(NS.r, 'embed') || blip.getAttribute('r:embed')
|
||
const relation = relationshipMap.get(relationshipId)
|
||
if (!relation) throw new Error(`Image relationship not found: ${relationshipId}`)
|
||
const target = relation.getAttribute('Target').replace(/^\.\//, '')
|
||
const screenshotPath = path.resolve(screenshotsDir, page.screenshot)
|
||
zip.file(`word/${target}`, fs.readFileSync(screenshotPath))
|
||
replacedScreenshots++
|
||
}
|
||
|
||
let bodyChildren = elementChildren(body)
|
||
const firstFilterHeadingIndex = bodyChildren.findIndex(node => localName(node) === 'p' && /\.1 搜索与过滤$/.test(textOf(node)))
|
||
const firstFilterTable = bodyChildren.slice(firstFilterHeadingIndex + 1).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 3)
|
||
const firstButtonHeadingIndex = bodyChildren.findIndex(node => localName(node) === 'p' && /\.3 功能按钮与操作$/.test(textOf(node)))
|
||
const firstButtonTable = bodyChildren.slice(firstButtonHeadingIndex + 1).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 2)
|
||
const heading3Template = bodyChildren[firstButtonHeadingIndex]
|
||
const bodyParagraphTemplate = bodyChildren.find(node => localName(node) === 'p' && textOf(node).length > 30 && !firstDescendant(node, 'blip'))
|
||
if (!firstFilterTable || !firstButtonTable || !heading3Template || !bodyParagraphTemplate) throw new Error('Required document templates not found')
|
||
|
||
// Remove internal implementation rows and notes from all generated business pages.
|
||
for (const table of descendants(body, 'tbl')) {
|
||
for (const row of [...elementChildren(table, 'tr')]) {
|
||
const firstCell = cellTexts(row)[0] || ''
|
||
if (['页面路由', '只读查询过程/接口', '写入过程/接口'].includes(firstCell)) table.removeChild(row)
|
||
if (firstCell === '页面输入') setCellText(document, elementChildren(row, 'tc')[0], '操作输入与上游条件')
|
||
if (firstCell === '页面输出') setCellText(document, elementChildren(row, 'tc')[0], '页面结果与下游影响')
|
||
}
|
||
}
|
||
for (const table of [...descendants(body, 'tbl')]) {
|
||
const value = textOf(table)
|
||
if (value.startsWith('技术核对说明:')) table.parentNode.removeChild(table)
|
||
if (value.includes('截图仅执行登录、页面导航和默认查询')) {
|
||
const cell = firstDescendant(table, 'tc')
|
||
if (cell) setCellText(document, cell, '本章范围:本章按正式菜单顺序说明各功能页面,重点介绍查询条件、列表字段、业务操作、弹窗和上下游数据关系。')
|
||
}
|
||
}
|
||
|
||
replaceAllTextNodes(document, [
|
||
['正式页面', '系统页面'],
|
||
['(正式环境只读截图)', '(系统页面)'],
|
||
['点击【取消】不产生数据。', '如不保存,点击【取消】关闭窗口。'],
|
||
['点击【取消】不会产生数据。', '如不保存,点击【取消】关闭窗口。'],
|
||
['由页面一并传入后台查询', '由系统一并用于查询'],
|
||
['以对应页面后台过程为准', '以页面实际查询结果为准'],
|
||
['本文档编制未实际执行这些按钮;', ''],
|
||
['本文档编制未实际执行这些按钮。', '']
|
||
])
|
||
|
||
// Replace every page's generic button list with code-backed action results and business impact.
|
||
let actionTablesRewritten = 0
|
||
for (const page of catalog) {
|
||
const section = `${chapterByModule[page.module]}.${page.pageIndex}`
|
||
bodyChildren = elementChildren(body)
|
||
const headingIndex = findDirectParagraphIndex(bodyChildren, `${section}.3 功能按钮与操作`)
|
||
if (headingIndex < 0) continue
|
||
const nextHeadingIndex = bodyChildren.findIndex((node, index) =>
|
||
index > headingIndex && localName(node) === 'p' && new RegExp(`^${section.replace('.', '\\.')}\\.\\d+ `).test(textOf(node))
|
||
)
|
||
const limit = nextHeadingIndex >= 0 ? nextHeadingIndex : bodyChildren.length
|
||
const oldTable = bodyChildren.slice(headingIndex + 1, limit).find(node => localName(node) === 'tbl')
|
||
const newTable = makeTable(document, firstFilterTable, actionRows(page))
|
||
if (oldTable) body.replaceChild(newTable, oldTable)
|
||
else body.insertBefore(newTable, bodyChildren[limit] || null)
|
||
actionTablesRewritten++
|
||
}
|
||
|
||
// Add actual dialog/form coverage to every business page that exposes dialogs.
|
||
let dialogSectionsAdded = 0
|
||
const fallbackDialogPages = []
|
||
for (const item of dialogAudit) {
|
||
if (item.title === '机加件已排产' || !item.dialogs.length) continue
|
||
const catalogPage = catalog.find(page => page.module === item.module && page.title === item.title)
|
||
if (!catalogPage) continue
|
||
const section = `${chapterByModule[item.module]}.${catalogPage.pageIndex}`
|
||
bodyChildren = elementChildren(body)
|
||
const sectionPattern = new RegExp(`^${section.replace('.', '\\.')}\\.(\\d+) 前后数据关系$`)
|
||
const relationshipIndex = bodyChildren.findIndex(node => localName(node) === 'p' && sectionPattern.test(textOf(node)))
|
||
let insertBeforeNode
|
||
let dialogNumber
|
||
let relationshipHeading = null
|
||
if (relationshipIndex >= 0) {
|
||
relationshipHeading = bodyChildren[relationshipIndex]
|
||
dialogNumber = Number(textOf(relationshipHeading).match(sectionPattern)[1])
|
||
insertBeforeNode = relationshipHeading
|
||
} else {
|
||
const pageStart = findDirectParagraphIndex(bodyChildren, `${section} ${item.title}`)
|
||
const pageEnd = bodyChildren.findIndex((node, index) =>
|
||
index > pageStart && localName(node) === 'p' && /^\d+\.\d+ /.test(textOf(node))
|
||
)
|
||
if (pageStart < 0) throw new Error(`Dialog page heading not found: ${section} ${item.title}`)
|
||
const end = pageEnd >= 0 ? pageEnd : bodyChildren.length - 1
|
||
const subsectionPattern = new RegExp(`^${section.replace('.', '\\.')}\\.(\\d+) `)
|
||
const subsectionNumbers = bodyChildren.slice(pageStart + 1, end)
|
||
.filter(node => localName(node) === 'p' && subsectionPattern.test(textOf(node)))
|
||
.map(node => Number(textOf(node).match(subsectionPattern)[1]))
|
||
dialogNumber = Math.max(3, ...subsectionNumbers) + 1
|
||
insertBeforeNode = bodyChildren[end]
|
||
fallbackDialogPages.push(`${section} ${item.title}`)
|
||
}
|
||
const rows = [['弹窗/操作', '可编辑或查看内容', '操作结果与影响'], ...dialogRows(item, catalogPage)]
|
||
const nodes = [
|
||
makeParagraph(document, heading3Template, `${section}.${dialogNumber} 弹窗与可编辑内容`),
|
||
makeParagraph(document, bodyParagraphTemplate, '点击列表中的新增、编辑、查看或业务操作后,系统会打开相应弹窗。下表说明各窗口的用途、确认后的结果以及对后续业务的影响。'),
|
||
makeTable(document, firstFilterTable, rows)
|
||
]
|
||
for (const node of nodes) body.insertBefore(node, insertBeforeNode)
|
||
if (relationshipHeading) setParagraphText(document, relationshipHeading, `${section}.${dialogNumber + 1} 前后数据关系`)
|
||
dialogSectionsAdded++
|
||
}
|
||
|
||
// Rewrite section 9.3 from the production-process component and its actual data effects.
|
||
bodyChildren = elementChildren(body)
|
||
let processSectionStart = findDirectParagraphIndex(bodyChildren, '9.3 生产工艺')
|
||
let processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询')
|
||
if (processSectionStart < 0 || processSectionEnd < 0) throw new Error('Section 9.3 boundaries not found')
|
||
setParagraphText(document, bodyChildren[processSectionStart + 1], '按生产订单编制和启用本次生产使用的工艺路线,可由标准工艺批量生成,也可在“编辑生产工艺”窗口中调整工序、工时、工装量具、机床和车间,确认后再下发为可执行的生产任务。')
|
||
|
||
bodyChildren = elementChildren(body)
|
||
const processButtonHeadingIndex = findDirectParagraphIndex(bodyChildren, '9.3.3 功能按钮与操作')
|
||
processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询')
|
||
const processButtonLimit = bodyChildren.findIndex((node, index) =>
|
||
index > processButtonHeadingIndex && index < processSectionEnd && localName(node) === 'p' && /^9\.3\.\d+ /.test(textOf(node))
|
||
)
|
||
const oldProcessButtonTable = bodyChildren.slice(processButtonHeadingIndex + 1, processButtonLimit >= 0 ? processButtonLimit : processSectionEnd)
|
||
.find(node => localName(node) === 'tbl')
|
||
const processButtonRows = [
|
||
['操作入口', '执行结果', '业务影响与注意事项'],
|
||
['查询', '按订单、物料、任务状态、属性、日期和工艺状态刷新列表。', '只刷新页面结果,不修改订单、工艺或任务数据。'],
|
||
['批量生成生产工艺', '对已勾选且“已有标准工艺、尚无生产工艺”的记录,将该物料的标准工艺复制为当前订单的生产工艺。', '只生成订单专用工艺,不等于下发生产任务;不符合条件的勾选行不会重复生成。'],
|
||
['编辑工艺', '打开当前订单和物料的“编辑生产工艺”窗口,读取原料信息、可选工序和已编制路线。', '打开窗口本身不修改数据;窗口内的行编辑和结构调整有各自的保存时机。']
|
||
]
|
||
if (oldProcessButtonTable) body.replaceChild(makeTable(document, firstFilterTable, processButtonRows), oldProcessButtonTable)
|
||
|
||
bodyChildren = elementChildren(body)
|
||
processSectionStart = findDirectParagraphIndex(bodyChildren, '9.3 生产工艺')
|
||
processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询')
|
||
const genericProcessDialogIndex = bodyChildren.findIndex((node, index) =>
|
||
index > processSectionStart && index < processSectionEnd && localName(node) === 'p' && /^9\.3\.\d+ 弹窗与可编辑内容$/.test(textOf(node))
|
||
)
|
||
if (genericProcessDialogIndex >= 0) {
|
||
for (const node of bodyChildren.slice(genericProcessDialogIndex, processSectionEnd)) body.removeChild(node)
|
||
}
|
||
bodyChildren = elementChildren(body)
|
||
const processInsertBefore = bodyChildren[findDirectParagraphIndex(bodyChildren, '9.4 工艺查询')]
|
||
const processEditRows = [
|
||
['区域/字段', '操作方法', '保存时机与实际影响'],
|
||
['左侧工序列表', '可先输入工序名称过滤,再单击所需工序。', '没有插入占位行时,所选工序追加到当前路线末尾;存在插入占位行时,所选工序填入该位置。单击后即写入当前订单工艺,并将工艺置为需重新保存的状态。'],
|
||
['准备工时、标准工时、工时合计', '在右侧当前工序行直接输入数值。', '输入变化时立即保存到该工序,用于后续工时核算和任务执行;顶部【保存】不是这些字段的统一提交按钮。'],
|
||
['工装量具、指定机床、车间、属性', '在右侧当前工序行选择对应项目。', '选择变化后立即保存,作为后续排产、设备/工位匹配和生产执行的工艺条件。'],
|
||
['工序说明、备注', '在右侧当前工序行输入,离开输入框时完成更新。', '完成输入后立即保存,用于传递工艺要求和执行注意事项。'],
|
||
['上移/下移', '点击当前工序行右侧的上移或下移图标。', '当前工序与相邻工序交换顺序,并立即刷新路线;调整后工艺需重新【保存】启用。'],
|
||
['插入', '在目标工序行点击插入图标,再到左侧单击要插入的工序。', '系统先在当前位置建立空占位行并后移原有顺序;必须再从左侧选择工序填入,不要留下空工序。完成后需重新【保存】启用。'],
|
||
['删除', '点击当前工序行右侧的删除图标。', '删除该工序并重排后续顺序;已下发任务时应先确认现场执行情况。删除后需重新【保存】启用。']
|
||
]
|
||
const processEffectRows = [
|
||
['操作', '点击后实际完成的事项', '对后续业务的影响/注意事项'],
|
||
['保存', '将当前订单的生产工艺标记为已启用,并更新计划记录的“生产工艺”状态。', '它的作用是确认当前工艺路线可用,不是保存右侧每个字段,也不会自动生成生产任务。新增、移动、插入或删除工序后应点击此按钮。'],
|
||
['保存为标准工艺', '将当前订单的整条生产工艺复制为该物料的标准工艺,并替换该物料原有的标准工艺路线。', '以后对同物料使用“批量生成生产工艺”时,会以这份新标准为模板。该操作会覆盖旧标准,应由有权限人员在完整复核后执行。'],
|
||
['下发任务(顶部)', '按当前工艺路线的全部工序生成生产任务,并将订单任务状态更新为已下发。', '下发后各工序进入生产排产、派工和执行环节;系统会识别已下发状态,避免重复生成整套任务。应在路线完整且已保存启用后执行。'],
|
||
['下发任务(单工序行)', '只为当前选定工序生成一条生产任务,并维护后续任务顺序。', '用于补发特定工序,不是整条路线下发的常规入口;使用前应检查该工序是否已有任务,避免重复。'],
|
||
['零件图纸/工艺图纸', '打开当前物料已上传的对应 PDF 图纸。', '只用于核对图纸,不修改工艺或任务数据;页面提示未上传时,应联系图纸维护人员。']
|
||
]
|
||
const processRelationRows = [
|
||
['数据关系', '业务说明'],
|
||
['上游业务数据', '使用生产订单、物料、计划数量、物料原料信息、工序库、设备/车间主数据以及该物料已有的标准工艺。'],
|
||
['本页形成的数据', '为指定生产订单形成一条订单专用生产工艺,包括工序顺序、工时、工装量具、指定机床、车间、属性和工艺要求。'],
|
||
['下游业务影响', '已启用的生产工艺可进一步下发为生产任务,供排产、派工、加工、工时和质量环节使用;保存为标准工艺后,还会影响同物料以后的工艺批量生成。']
|
||
]
|
||
bodyChildren = elementChildren(body)
|
||
processSectionStart = findDirectParagraphIndex(bodyChildren, '9.3 生产工艺')
|
||
processSectionEnd = findDirectParagraphIndex(bodyChildren, '9.4 工艺查询')
|
||
const processMainImageParagraph = bodyChildren.slice(processSectionStart, processSectionEnd)
|
||
.find(node => localName(node) === 'p' && firstDescendant(node, 'blip'))
|
||
if (!processMainImageParagraph) throw new Error('Section 9.3 main screenshot not found')
|
||
const processPopupImageParagraph = processMainImageParagraph.cloneNode(true)
|
||
const processPopupRelationshipId = `rId${++maxRelationshipId}`
|
||
const processPopupTarget = 'media/mes-9-3-process-edit-dialog.png'
|
||
const processPopupRelation = relationships.createElementNS(NS.rel, 'Relationship')
|
||
processPopupRelation.setAttribute('Id', processPopupRelationshipId)
|
||
processPopupRelation.setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image')
|
||
processPopupRelation.setAttribute('Target', processPopupTarget)
|
||
relationships.documentElement.appendChild(processPopupRelation)
|
||
firstDescendant(processPopupImageParagraph, 'blip').setAttributeNS(NS.r, 'r:embed', processPopupRelationshipId)
|
||
zip.file(`word/${processPopupTarget}`, fs.readFileSync(path.resolve(screenshotsDir, '03-03-process-edit-dialog.png')))
|
||
const processMainImageIndex = bodyChildren.indexOf(processMainImageParagraph)
|
||
const processCaptionTemplate = bodyChildren.find((node, index) =>
|
||
index > processMainImageIndex && index < processSectionEnd && localName(node) === 'p' && /^图 9\.3-1/.test(textOf(node))
|
||
) || bodyParagraphTemplate
|
||
const processNodes = [
|
||
makeParagraph(document, heading3Template, '9.3.4 编辑生产工艺与保存时机'),
|
||
makeParagraph(document, bodyParagraphTemplate, '“编辑生产工艺”窗口左侧是可选工序库,右侧是当前订单实际使用的工艺路线。右侧行内字段在输入或选择变化时保存;新增、移动、插入或删除工序会改变路线结构,完成后还应点击顶部【保存】将路线重新启用。'),
|
||
processPopupImageParagraph,
|
||
makeParagraph(document, processCaptionTemplate, '图 9.3-2 生产工艺-编辑生产工艺弹窗'),
|
||
makeTable(document, firstFilterTable, processEditRows),
|
||
makeParagraph(document, heading3Template, '9.3.5 顶部与行内操作结果'),
|
||
makeTable(document, firstFilterTable, processEffectRows),
|
||
makeParagraph(document, heading3Template, '9.3.6 前后数据关系'),
|
||
makeTable(document, firstButtonTable, processRelationRows)
|
||
]
|
||
for (const node of processNodes) body.insertBefore(node, processInsertBefore)
|
||
|
||
// Rewrite section 10.3 from the actual current page behavior.
|
||
bodyChildren = elementChildren(body)
|
||
const sectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产')
|
||
const sectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产')
|
||
if (sectionStart < 0 || sectionEnd < 0) throw new Error('Section 10.3 boundaries not found')
|
||
setParagraphText(document, bodyChildren[sectionStart + 1], '查询并维护已完成排产的机加任务,可按订单、物料、工序、要求完工日期和计划日期筛选,并在列表中调整工位、数量、计划时间、外协分组及备注信息。')
|
||
|
||
bodyChildren = elementChildren(body)
|
||
const searchHeadingIndex = findDirectParagraphIndex(bodyChildren, '10.3.1 搜索与过滤')
|
||
const listHeadingIndex = findDirectParagraphIndex(bodyChildren, '10.3.2 列表字段与页面结果')
|
||
const oldSearchTable = bodyChildren.slice(searchHeadingIndex + 1, listHeadingIndex).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 3)
|
||
const newSearchRows = [
|
||
['条件', '控件形式', '使用说明'],
|
||
['生产订单', '文本输入', '输入生产订单号,支持与其他条件组合查询。'],
|
||
['销售订单', '文本输入', '输入销售订单号,定位对应合同任务。'],
|
||
['物料编码', '文本输入', '输入完整或部分物料编码。'],
|
||
['物料名称', '文本输入', '输入完整或部分物料名称。'],
|
||
['工序名称', '文本输入', '输入工序名称,定位指定加工环节。'],
|
||
['要求完工日期', '日期范围', '选择开始和结束日期,筛选要求在该区间完工的任务。'],
|
||
['计划开始', '日期选择', '筛选计划开始日期不早于所选日期的任务。'],
|
||
['计划完成', '日期选择', '筛选计划完成日期不晚于所选日期的任务。'],
|
||
['工位复选框', '多选', '勾选一个或多个工位后缩小任务范围;未勾选时查询全部工位。']
|
||
]
|
||
if (oldSearchTable) body.replaceChild(makeTable(document, firstFilterTable, newSearchRows), oldSearchTable)
|
||
|
||
bodyChildren = elementChildren(body)
|
||
const functionSectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产')
|
||
const functionSectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产')
|
||
const functionHeadingIndex = findDirectParagraphIndex(bodyChildren, '10.3.3 功能按钮与操作')
|
||
const relationshipHeadingIndex = bodyChildren.findIndex((node, index) =>
|
||
index > functionHeadingIndex && index < functionSectionEnd && localName(node) === 'p' && /^10\.3\.[45] 前后数据关系$/.test(textOf(node))
|
||
)
|
||
const functionSectionLimit = relationshipHeadingIndex >= 0 ? relationshipHeadingIndex : functionSectionEnd
|
||
const oldButtonTable = bodyChildren.slice(functionHeadingIndex + 1, functionSectionLimit).find(node => localName(node) === 'tbl' && elementChildren(elementChildren(node, 'tr')[0] || {}, 'tc').length === 2)
|
||
const newButtonRows = [
|
||
['按钮/区域', '操作说明'],
|
||
['查询', '按当前订单、物料、工序、日期和工位条件刷新列表。'],
|
||
['表单', '以表格方式查看任务,并在可编辑列中直接维护排产信息。'],
|
||
['日程', '按日历查看计划;拖动或调整日程会修改任务的计划开始和计划完成日期。'],
|
||
['原料状态-查看', '打开原料信息弹窗,查看子件编码、数量、库存和材质。'],
|
||
['编辑', '打开“编辑备注信息”弹窗,维护优先级、未完成说明和派工特殊备注。'],
|
||
['分页与横向滚动', '使用分页切换记录;向右滚动可查看二次派工、最晚开始、编辑和最后编辑信息。']
|
||
]
|
||
if (oldButtonTable) body.replaceChild(makeTable(document, firstButtonTable, newButtonRows), oldButtonTable)
|
||
|
||
bodyChildren = elementChildren(body)
|
||
const refreshedSectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产')
|
||
const refreshedSectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产')
|
||
let relationshipIndex = bodyChildren.findIndex((node, index) =>
|
||
index > refreshedSectionStart && index < refreshedSectionEnd && localName(node) === 'p' && /^10\.3\.[45] 前后数据关系$/.test(textOf(node))
|
||
)
|
||
let relationshipHeading = bodyChildren[relationshipIndex]
|
||
if (!relationshipHeading) {
|
||
const sectionTables = bodyChildren.slice(functionHeadingIndex + 1, refreshedSectionEnd).filter(node => localName(node) === 'tbl')
|
||
const relationshipTable = sectionTables[sectionTables.length - 1]
|
||
relationshipHeading = makeParagraph(document, heading3Template, '10.3.4 前后数据关系')
|
||
body.insertBefore(relationshipHeading, relationshipTable || bodyChildren[refreshedSectionEnd])
|
||
bodyChildren = elementChildren(body)
|
||
relationshipIndex = bodyChildren.indexOf(relationshipHeading)
|
||
}
|
||
const editRows = [
|
||
['字段/入口', '操作方法', '功能解释与注意事项'],
|
||
['指派对象', '单击当前工位,在下拉列表选择新工位。', '用于调整任务执行工位;选择后立即保存。'],
|
||
['指派数量', '单击数量,输入新值后点击行内【确定】。', '数量必须大于等于1且不能超过计划数量。'],
|
||
['计划开始', '在当前行日期框选择日期。', '表示任务预计开始加工的日期;修改后立即保存,并影响日程视图及开工及时性判断。'],
|
||
['计划完成', '在当前行日期框选择日期。', '表示任务计划完成的日期;应不早于计划开始,修改后立即保存。'],
|
||
['外协分组', '在当前行输入分组值,完成输入后触发保存。', '用于给外协任务标记同一协同批次;应按计划部门既定分组规则填写,不要随意使用测试编号。'],
|
||
['二次派工', '在多选下拉框中选择参与人员。', '用于补充协作人员;选择变化后立即保存。'],
|
||
['最晚开始', '在当前行日期框选择日期。', '表示为满足完工要求允许的最迟开工日期;修改后立即保存。'],
|
||
['编辑', '点击【编辑】,在弹窗中维护优先级、未完成说明和派工特殊备注,再点击【确定】。', '只有弹窗中的三个备注类字段由【确定】统一提交;点击【取消】不保存本次弹窗修改。']
|
||
]
|
||
const intro = '本页有两种修改方式:计划开始、计划完成、外协分组等字段在表格中直接修改;【编辑】按钮只打开备注弹窗。表格内多数控件在选择或输入完成后立即保存,没有统一的【保存】按钮,因此操作前必须先确认当前行。'
|
||
const specialNodes = [
|
||
makeParagraph(document, heading3Template, '10.3.4 可编辑字段与编辑弹窗'),
|
||
makeParagraph(document, bodyParagraphTemplate, intro),
|
||
makeTable(document, firstFilterTable, editRows)
|
||
]
|
||
|
||
const mainImageParagraph = elementChildren(body).slice(sectionStart, sectionEnd).find(node => localName(node) === 'p' && firstDescendant(node, 'blip'))
|
||
const popupImageParagraph = mainImageParagraph.cloneNode(true)
|
||
const popupRelationshipId = `rId${++maxRelationshipId}`
|
||
const popupTarget = 'media/mes-10-3-edit-dialog.png'
|
||
const popupRelation = relationships.createElementNS(NS.rel, 'Relationship')
|
||
popupRelation.setAttribute('Id', popupRelationshipId)
|
||
popupRelation.setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image')
|
||
popupRelation.setAttribute('Target', popupTarget)
|
||
relationships.documentElement.appendChild(popupRelation)
|
||
const popupBlip = firstDescendant(popupImageParagraph, 'blip')
|
||
popupBlip.setAttributeNS(NS.r, 'r:embed', popupRelationshipId)
|
||
zip.file(`word/${popupTarget}`, fs.readFileSync(path.resolve(screenshotsDir, '04-03-plan-edit-dialog.png')))
|
||
const mainImageIndex = elementChildren(body).indexOf(mainImageParagraph)
|
||
const captionTemplate = elementChildren(body).find((node, index) =>
|
||
index > mainImageIndex && index < refreshedSectionEnd && localName(node) === 'p' && /^图 10\.3-1/.test(textOf(node))
|
||
) || bodyParagraphTemplate
|
||
specialNodes.push(popupImageParagraph)
|
||
specialNodes.push(makeParagraph(document, captionTemplate, '图 10.3-2 机加件已排产-编辑备注信息弹窗'))
|
||
for (const node of specialNodes) body.insertBefore(node, relationshipHeading)
|
||
setParagraphText(document, relationshipHeading, '10.3.5 前后数据关系')
|
||
const relationshipRows = [
|
||
['数据关系', '业务说明'],
|
||
['上游业务数据', '使用生产订单、销售订单、物料与工序信息、工位设置以及已形成的排产任务,页面筛选条件不会改变上游原始订单和主数据。'],
|
||
['本页维护结果', '可调整任务工位、指派数量、计划开始、计划完成、外协分组、协作人员、最晚开始及备注信息;相应修改保存到当前排产任务。'],
|
||
['下游业务影响', '维护结果用于后续派工、日程查看、原料准备、生产执行和计划及时性判断;日期或工位调整后,相关岗位应以页面最新结果为准。']
|
||
]
|
||
const relationshipTable = makeTable(document, firstButtonTable, relationshipRows)
|
||
body.insertBefore(relationshipTable, relationshipHeading.nextSibling)
|
||
bodyChildren = elementChildren(body)
|
||
const relationshipHeadingPosition = bodyChildren.indexOf(relationshipHeading)
|
||
const nextPagePosition = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产')
|
||
const operationNoticeTable = bodyChildren.slice(relationshipHeadingPosition + 1, nextPagePosition)
|
||
.find(node => node !== relationshipTable && localName(node) === 'tbl')
|
||
if (operationNoticeTable) {
|
||
const noticeCell = firstDescendant(operationNoticeTable, 'tc')
|
||
if (noticeCell) setCellText(document, noticeCell, '操作注意:计划开始、计划完成、外协分组等表格字段在选择或输入完成后立即保存;【编辑】弹窗中的备注字段点击【确定】后保存。操作前应核对当前任务行和修改内容。')
|
||
}
|
||
|
||
// Make the section summary state that this page writes immediately.
|
||
bodyChildren = elementChildren(body)
|
||
const currentSectionStart = findDirectParagraphIndex(bodyChildren, '10.3 机加件已排产')
|
||
const currentSectionEnd = findDirectParagraphIndex(bodyChildren, '10.4 机加件未排产')
|
||
for (const table of bodyChildren.slice(currentSectionStart, currentSectionEnd).filter(node => localName(node) === 'tbl')) {
|
||
for (const row of elementChildren(table, 'tr')) {
|
||
const values = cellTexts(row)
|
||
if (values[0] === '数据性质' && values[1] && values[1].includes('业务按钮可能')) {
|
||
setCellText(document, elementChildren(row, 'tc')[1], '支持查询及排产信息维护;表格内修改通常即时保存')
|
||
}
|
||
}
|
||
}
|
||
|
||
// Append a revision record entry.
|
||
const revisionTable = descendants(body, 'tbl').find(table => {
|
||
const firstRow = elementChildren(table, 'tr')[0]
|
||
return firstRow && cellTexts(firstRow)[0] === '版本'
|
||
})
|
||
if (revisionTable) {
|
||
const rows = elementChildren(revisionTable, 'tr')
|
||
const newRow = rows[rows.length - 1].cloneNode(true)
|
||
setRowValues(document, newRow, ['V1.1', '2026-07-24', '按用户反馈复核修订', '更新69页系统截图和操作结果/业务影响说明,补充44页弹窗内容,详细重写9.3生产工艺与10.3机加已排产,移除内部实现名称和编制过程描述'])
|
||
revisionTable.appendChild(newRow)
|
||
}
|
||
|
||
const coreEntry = zip.file('docProps/core.xml')
|
||
if (coreEntry) {
|
||
const core = parser.parseFromString(coreEntry.asText(), 'application/xml')
|
||
const modified = descendants(core.documentElement, 'modified')[0]
|
||
if (modified) modified.textContent = new Date().toISOString()
|
||
zip.file('docProps/core.xml', serializer.serializeToString(core))
|
||
}
|
||
|
||
zip.file('word/document.xml', serializer.serializeToString(document))
|
||
zip.file('word/_rels/document.xml.rels', serializer.serializeToString(relationships))
|
||
const output = zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' })
|
||
fs.writeFileSync(stagingPath, output)
|
||
|
||
// Reopen the generated package before replacing the requested document.
|
||
const validationZip = new JSZip(fs.readFileSync(stagingPath))
|
||
const validationDocument = parser.parseFromString(validationZip.file('word/document.xml').asText(), 'application/xml')
|
||
const validationText = textOf(validationDocument.documentElement)
|
||
if (!validationText.includes('10.3.4 可编辑字段与编辑弹窗')) throw new Error('Revised section 10.3 was not written')
|
||
if (!validationText.includes('编辑备注信息弹窗')) throw new Error('Edit dialog caption was not written')
|
||
if (!validationText.includes('图 9.3-2 生产工艺-编辑生产工艺弹窗')) throw new Error('Production process dialog caption was not written')
|
||
fs.copyFileSync(stagingPath, docxPath)
|
||
|
||
console.log(`Replaced screenshots: ${replacedScreenshots}`)
|
||
console.log(`Rewritten action tables: ${actionTablesRewritten}`)
|
||
console.log(`Added dialog sections: ${dialogSectionsAdded + 1}`)
|
||
if (fallbackDialogPages.length) console.log(`Fallback dialog pages: ${fallbackDialogPages.join(' | ')}`)
|
||
console.log(`Backup: ${backupPath}`)
|
||
console.log(`DOCX: ${docxPath}`)
|
||
}
|
||
|
||
main().catch(error => {
|
||
console.error(error.stack || error.message)
|
||
process.exitCode = 1
|
||
})
|