Files
MES_Manage_View_V20/work/system-manual/revise-full-manual.js

799 lines
54 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
})