Files
MES_Manage_View_V20/work/generate-mes-user-manual.js

468 lines
46 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 root = path.resolve(__dirname, '..')
const docDir = path.join(root, 'doc')
const menuRows = parseCsv(fs.readFileSync(path.join(root, 'work', 'menu_inventory_role1.csv'), 'utf8'))
const pageRows = parseCsv(fs.readFileSync(path.join(root, 'work', 'page_ui_inventory_role1_20260725.csv'), 'utf8'))
const frontendRows = parseCsv(fs.readFileSync(path.join(root, 'work', 'frontend_procedure_call_inventory_20260725.csv'), 'utf8'))
const dbRows = parseCsv(fs.readFileSync(path.join(root, 'work', 'database_procedure_inventory_20260725.csv'), 'utf8'))
const dependencyRows = parseCsv(fs.readFileSync(path.join(root, 'work', 'database_expression_dependencies_20260725.csv'), 'utf8'))
const missingRows = parseCsv(fs.readFileSync(path.join(root, 'work', 'frontend_calls_missing_db_procedure_20260725.csv'), 'utf8'))
const scanSummary = JSON.parse(fs.readFileSync(path.join(root, 'work', 'frontend_function_scan_summary_20260725.json'), 'utf8'))
function parseCsv(text) {
const source = text.replace(/^\uFEFF/, '').trim()
const records = []
let record = []
let value = ''
let quoted = false
for (let i = 0; i < source.length; i++) {
const c = source[i]
if (c === '"') {
if (quoted && source[i + 1] === '"') {
value += '"'
i++
} else {
quoted = !quoted
}
} else if (c === ',' && !quoted) {
record.push(value)
value = ''
} else if ((c === '\r' || c === '\n') && !quoted) {
if (c === '\r' && source[i + 1] === '\n') i++
record.push(value)
records.push(record)
record = []
value = ''
} else {
value += c
}
}
record.push(value)
records.push(record)
const headers = records.shift()
return records
.filter(values => values.some(item => item !== ''))
.map(values => Object.fromEntries(headers.map((header, i) => [header, values[i] || ''])))
}
function unique(values) {
return [...new Set(values.map(value => String(value || '').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()).filter(Boolean))]
}
function list(value, limit = 10) {
const values = unique(String(value || '').split('|')).filter(item => !/^(if|for|returnModule|fn|-->)/.test(item))
return values.slice(0, limit).join('、') + (values.length > limit ? ' 等' : '')
}
function csv(value) {
return `"${String(value == null ? '' : value).replace(/"/g, '""')}"`
}
const rootRows = menuRows.filter(row => row.pid === '0')
const pagesByModule = new Map()
for (const row of pageRows) {
if (!pagesByModule.has(row.module)) pagesByModule.set(row.module, [])
pagesByModule.get(row.module).push(row)
}
const moduleTitles = Object.fromEntries(rootRows.map(row => [row.componet.split('/')[1], row.title]))
const moduleIntros = {
SystemMaintenance: '维护人员、角色、菜单、工位和操作记录,是权限与基础配置入口。',
PlanManagement: '管理机加件、装配件、工位任务、排产状态、优先级、关闭和齐套跟踪。',
ProductionManagement: '覆盖生产计划跟踪、加工中心、装配、工时、外协、拣配、设备状态和发货通知。',
QualityManagement: '覆盖终检、序检、采购/退库/其他入库检、质检报工、质检明细和不合格品处置。',
ProcessManagement: '维护工序、标准工艺、生产工艺、工艺任务、零件图纸和工艺图纸。',
DeviceManagement: '维护设备基础信息、工位绑定、设备采集数据、历史数据和实时数据。',
SaleManagement: '查看销售订单状态、排产、发货和入库相关信息。',
AnomalousManagement: '集中查看未及时开工、外协开工、加急任务和装配及时送检异常。',
ResearchManagement: '覆盖设计任务、设计报工、设计工时校验、工时报表和研发财务工时。'
}
function moduleWorkflow(module) {
const workflows = {
SystemMaintenance: ['先维护人员和角色,再配置菜单权限;修改权限后退出并重新登录验证。', '工位管理用于维护 MES 工位名称和绑定信息,删除或修改前确认没有未完成任务。', '操作记录用于追溯关键人工操作,出现争议时先按时间、人员和模块筛选。'],
PlanManagement: ['先按订单、物料、日期和状态查询,再选择任务执行排产、派工、优先级或计划时间调整。', '批量操作前核对选择行、计划数量和指派对象;排产完成后到已排产/工位任务汇总复核。', '系统类装配任务和阀类装配任务中的预算工时为手工输入,进度由任务/质检工时除以对应预算自动计算。'],
ProductionManagement: ['加工中心按待处理任务、工序和工位操作,通常按收料、开工、暂停/恢复、报工、送检或异常流程推进。', '工时修正、结算工时和外协流程属于受控写操作,必须确认业务单据和审批状态。', '生产计划跟踪、工时报表、装配状态和发货通知优先用于查询和核对。'],
QualityManagement: ['先选择质检任务和质检类型,再录入实测值、判定、合格/不合格数量并保存;不合格品进入处置流程。', '终检、序检、收检、采购入库检、退库检和其他入库检应按实际业务节点使用,不要跨节点补录。', '质检明细、及时检验报表和产品合格率用于追溯和统计,导出前先确认筛选条件。'],
ProcessManagement: ['先维护工序和标准工艺,再生成/编辑生产工艺,最后向生产订单下发工艺任务。', '图纸维护和工艺维护用于查看、上传或关联图纸;修改工序顺序、工时和启用状态前确认影响范围。', '工艺查询优先用于只读核对,工艺编辑需保留变更原因。'],
DeviceManagement: ['先建立设备基础信息并绑定工位,再使用设备数据、历史数据和实时数据检查采集状态。', '设备信息中的证书有效期、主要设备、特种设备和标准开机工时应保持准确。', '实时数据异常时先判断采集链路,再判断页面和数据库,不要直接重复提交设备写操作。'],
SaleManagement: ['使用订单号、合同号、产品编码、计划日期、排产/发货/入库状态筛选销售订单。', '订单状态用于跨计划、生产、库存和发货的核对,不作为单独修改生产状态的入口。'],
AnomalousManagement: ['先按异常类型、订单、产品和时间筛选,确认责任工序和当前状态。', '加急任务和及时送检异常处理前先确认计划、工序、人员和优先级,避免重复催办。'],
ResearchManagement: ['设计任务用于接收和分派研发任务,设计报工用于提交研发工时,设计工时校验和报表用于复核。', '研发财务工时属于统计口径页面,修改前需确认研发任务和财务归集规则。']
}
return workflows[module] || ['按页面筛选条件查询数据,选择记录后执行页面提供的操作。', '涉及保存、删除、状态切换的操作必须先确认权限和回滚方式。']
}
const markdown = []
markdown.push('# 大连元利流体技术有限公司 MES 后台网页版用户操作手册')
markdown.push('')
markdown.push('**文档版本V1.0 | 适用环境MES 测试环境 | 日期2026-07-25**')
markdown.push('')
markdown.push('> 本手册基于测试环境角色 1 的 84 条菜单、75 个可进入页面、前端代码静态扫描和测试数据库对象盘点生成。不同角色的菜单、按钮和数据范围以实际权限为准。')
markdown.push('')
markdown.push('## 1. 使用前说明')
markdown.push('')
markdown.push('- 测试前端地址:`http://124.220.15.242:10012/`。')
markdown.push('- MES 接口由前端自动调用,用户不需要手工拼接接口地址。')
markdown.push('- 测试环境 B1/TM 只允许登录与只读 GET语义 POST/PATCH 会在前端拦截,不要用写操作验证连通性。')
markdown.push('- 页面按钮和数据范围由角色权限控制;看不到某个菜单或按钮时先联系系统管理员,不要直接修改浏览器参数。')
markdown.push('- 涉及新增、编辑、删除、排产、派工、报工、质检、入库和状态切换的操作,必须先确认业务单据、操作人和回滚方式。')
markdown.push('')
markdown.push('## 2. 登录与通用操作')
markdown.push('')
markdown.push('### 2.1 登录')
markdown.push('')
markdown.push('1. 打开测试前端地址。')
markdown.push('2. 输入账号和密码,点击“登录”。')
markdown.push('3. 登录成功后进入首页;系统按角色加载菜单和按钮权限。')
markdown.push('4. “记住账号”只保存账号,不保存密码;离开共享电脑时不要勾选。')
markdown.push('')
markdown.push('### 2.2 列表查询')
markdown.push('')
markdown.push('1. 先填写必要筛选条件,日期条件使用页面提供的日期选择器。')
markdown.push('2. 点击“查询”,等待表格刷新。')
markdown.push('3. 使用分页、每页条数、排序和导出功能核对结果。')
markdown.push('4. 查询结果为空时先清空筛选条件,再判断是否确实没有数据。')
markdown.push('')
markdown.push('### 2.3 新增、编辑、删除与状态操作')
markdown.push('')
markdown.push('1. 新增前确认必填字段、编码唯一性和关联对象。')
markdown.push('2. 编辑前先选中正确记录,核对订单、合同、工位或人员。')
markdown.push('3. 删除、关闭、完成、启用、禁用、排产、派工和报工属于状态性操作,执行前必须二次确认。')
markdown.push('4. 保存后重新查询并核对操作时间、编辑人和状态。')
markdown.push('')
markdown.push('### 2.4 页面常见问题')
markdown.push('')
markdown.push('| 现象 | 处理顺序 |')
markdown.push('| --- | --- |')
markdown.push('| 登录成功但没有菜单 | 退出重新登录;确认角色是否启用、菜单是否分配、接口是否返回菜单数据。 |')
markdown.push('| 查询很慢 | 缩小日期和订单范围;记录页面、条件和耗时,交给运维分析存储过程和执行计划。 |')
markdown.push('| 按钮不存在 | 检查角色 AddData/DeleteData/EditData/SearchData 权限,不要直接改前端隐藏逻辑。 |')
markdown.push('| B1/TM 操作失败 | 测试环境先确认是否为被保护的 POST/PATCH写操作不会为了测试而放开。 |')
markdown.push('| 保存后页面没有变化 | 重新查询并查看接口返回;确认过程是否存在、参数名和业务状态是否满足。 |')
markdown.push('')
markdown.push('## 3. 菜单与页面总览')
markdown.push('')
markdown.push(`当前角色菜单共 **${menuRows.length}** 条,其中模块 **${rootRows.length}** 个、可进入页面 **${pageRows.length}** 个。`)
markdown.push('')
for (const rootRow of rootRows) {
const module = rootRow.componet.split('/')[1]
const pages = pagesByModule.get(module) || []
markdown.push(`### ${rootRow.title}`)
markdown.push('')
markdown.push(`用途:${moduleIntros[module] || '用于 MES 业务管理和查询。'}`)
markdown.push('')
markdown.push('| 页面 | 路径 | 主要操作 | 关键字段/内容 |')
markdown.push('| --- | --- | --- | --- |')
for (const page of pages) {
markdown.push(`| ${page.title} | \`${page.path}\` | ${list(page.buttons, 8) || '查询/查看'} | ${list(page.columns, 10) || '按页面实际数据展示'} |`)
}
markdown.push('')
}
markdown.push('## 4. 各模块操作流程')
markdown.push('')
for (const rootRow of rootRows) {
const module = rootRow.componet.split('/')[1]
markdown.push(`### 4.${rootRows.indexOf(rootRow) + 1} ${rootRow.title}`)
markdown.push('')
markdown.push(`模块定位:${moduleIntros[module] || '用于 MES 业务管理和查询。'}`)
for (const item of moduleWorkflow(module)) markdown.push(`- ${item}`)
markdown.push('')
const pages = pagesByModule.get(module) || []
for (const page of pages) {
markdown.push(`#### ${page.title}`)
markdown.push('')
markdown.push(`页面地址:\`${page.path}\``)
markdown.push('')
markdown.push(`- 页面源码:\`${page.source}\``)
markdown.push(`- 页面操作:${list(page.buttons, 14) || '查询、查看'}`)
markdown.push(`- 主要字段:${list(page.columns, 18) || '按接口返回字段展示'}`)
if (page.procedures) markdown.push(`- 相关过程:${list(page.procedures.split('|'), 18)}`)
if (page.b1GetPostPatch && page.b1GetPostPatch !== '0/0/0') markdown.push(`- B1 调用GET/POST/PATCH = ${page.b1GetPostPatch};测试环境 POST/PATCH 受保护。`)
if (page.tmGetPostPatch && page.tmGetPostPatch !== '0/0/0') markdown.push(`- TM 调用GET/POST/PATCH = ${page.tmGetPostPatch};测试环境写操作受保护。`)
markdown.push('操作建议:先查询并确认记录,再执行页面按钮;保存或状态操作后重新查询核对结果。')
markdown.push('')
}
}
markdown.push('## 5. 重点业务流程')
markdown.push('')
markdown.push('### 5.1 排产与装配任务')
markdown.push('')
markdown.push('1. 在“机加件排产”或“装配件排产”按订单、物料、日期和状态查询。')
markdown.push('2. 选择任务后执行排产、派工、优先级或计划时间调整;批量操作前核对选择行。')
markdown.push('3. 到“已排产”“工位任务汇总”或“及时齐套跟踪结果”复核状态。')
markdown.push('4. 在“系统类装配任务”“阀类装配任务”维护预算装配工时和合同预算测试工时,进度字段由系统计算。')
markdown.push('')
markdown.push('### 5.2 工艺维护与工艺下发')
markdown.push('')
markdown.push('1. 在“工序管理”维护工序基础信息。')
markdown.push('2. 在“制定标准工艺”维护准备工时、标准工时、工装量具、机床和车间。')
markdown.push('3. 在“生产工艺”查询订单并生成/编辑工艺,确认工序顺序和工时。')
markdown.push('4. 需要下发生产任务时,确认订单、工艺版本和目标工序后执行下发。')
markdown.push('')
markdown.push('### 5.3 加工中心与装配中心')
markdown.push('')
markdown.push('1. 按工位、人员、班次和任务状态查询待处理任务。')
markdown.push('2. 按业务顺序执行收料、开工、暂停/恢复、报工、送检和异常处理。')
markdown.push('3. 质检数据录入前核对检测项目、理论值、上下限、实测值和判定。')
markdown.push('4. 操作完成后检查任务状态、工时、数量和最后编辑人。')
markdown.push('')
markdown.push('### 5.4 质量检验与不合格品')
markdown.push('')
markdown.push('1. 根据业务节点进入终检、序检、收检、采购入库检、销售退货检、原材料退库检或其他入库检。')
markdown.push('2. 选择任务和检测项目,录入检验数量、合格数、不合格数和实测值。')
markdown.push('3. 不合格结果进入“不合格品处置”,填写处置结论并保留审批/操作记录。')
markdown.push('4. 使用“质检明细”“及时检验报表”“产品合格率”进行追溯和统计。')
markdown.push('')
markdown.push('### 5.5 人员、角色、菜单和工位')
markdown.push('')
markdown.push('1. 先新增或维护人员,再配置角色。')
markdown.push('2. 在角色管理为角色分配模块和操作权限。')
markdown.push('3. 在菜单管理维护模块路径和层级;变更菜单前先确认路由组件存在。')
markdown.push('4. 在工位管理维护工位号、名称和关联信息。')
markdown.push('5. 权限修改后使用目标账号重新登录验证,不以管理员账号代替验收。')
markdown.push('')
markdown.push('## 6. 权限与数据安全')
markdown.push('')
markdown.push('- 菜单权限来自数据库角色菜单过程;页面按钮受 AddData、DeleteData、EditData、SearchData 等权限控制。')
markdown.push('- 测试前端配置 `externalReadOnly=true` 时B1/TM 语义 POST/PATCH 在网络请求前被拦截。')
markdown.push('- 不要在浏览器开发者工具中手工重放生产接口或外部系统写请求。')
markdown.push('- 不要把账号、密码、Token、业务数据或导出文件提交到代码仓库。')
markdown.push('- 发现数据异常时保留页面、筛选条件、时间、账号、响应状态和操作记录,不要先删除数据。')
markdown.push('')
markdown.push('## 7. 功能定位与新增需求说明')
markdown.push('')
markdown.push('### 7.1 页面问题定位')
markdown.push('')
markdown.push('1. 先从菜单路径定位页面源码:`src/views/<模块>/<页面>/index.vue`,特殊页面以菜单组件路径为准。')
markdown.push('2. 在页面中查找 `CreateData`,确认过程名、调用类型和参数名。')
markdown.push('3. 通用接口入口位于 `src/utils/request.js` 和 `src/utils/curd.js`;动态菜单由 `src/router/getTreeData.js` 和 `src/router/menuComponentModule.js` 生成。')
markdown.push('4. B1/TM 封装分别位于 `src/api/b1s.js`、`src/api/tm.js`;测试环境不要绕过只读保护。')
markdown.push('5. 到数据库过程清单中确认过程存在、参数数量、写入特征和外部依赖,再决定改前端、改过程还是补充对象。')
markdown.push('')
markdown.push('### 7.2 新增需求的推荐顺序')
markdown.push('')
markdown.push('1. 明确页面角色、数据范围、查询条件、字段口径和读写边界。')
markdown.push('2. 先确认现有过程是否可复用,避免新增同义过程。')
markdown.push('3. 测试库新增/修改过程时编写前置校验、事务、验证 SQL 和回滚脚本。')
markdown.push('4. 前端实现表格、表单、权限按钮、加载/空结果/错误状态和分页。')
markdown.push('5. 先做只读回归,再用隔离数据做写流程回归,最后更新菜单、手册和变更记录。')
markdown.push('')
markdown.push('## 8. 全量扫描结果与已知风险')
markdown.push('')
markdown.push(`- 前端源码:${scanSummary.sourceFiles} 个源码文件,其中 ${scanSummary.vueFiles} 个 Vue、${scanSummary.jsFiles} 个 JS。`)
markdown.push(`- CreateData${scanSummary.totalCreateDataCalls} 次调用,${scanSummary.literalCreateDataCalls} 次字面量过程调用,${scanSummary.uniqueLiteralProcedures} 个唯一字面量过程名,另有 ${scanSummary.dynamicCreateDataCalls} 次动态调用需人工复核。`)
markdown.push(`- 外部集成调用B1 GET/POST/PATCH = ${scanSummary.b1.get}/${scanSummary.b1.post}/${scanSummary.b1.patch}TM GET/POST/PATCH = ${scanSummary.tm.get}/${scanSummary.tm.post}/${scanSummary.tm.patch}`)
markdown.push(`- 测试数据库:${dbRows.length} 个存储过程、63 张表、27 个视图;过程定义中包含明显写关键字的约 ${dbRows.filter(row => row.HasWriteKeyword === '1').length} 个。`)
markdown.push(`- 数据库表达式依赖:${dependencyRows.length} 条,其中跨服务器依赖 ${dependencyRows.filter(row => String(row.ReferencedServer || '').trim()).length} 条,跨库依赖 ${dependencyRows.filter(row => String(row.ReferencedDatabase || '').trim()).length} 条。`)
markdown.push(`- 前端调用但测试库找不到同名过程:${missingRows.length} 个,属于重点风险,不代表全部页面不可用,需按部署版本、动态过程或历史页面逐项确认。`)
if (missingRows.length) {
markdown.push('')
markdown.push('缺失过程清单:')
for (const row of missingRows) markdown.push(`- \`${row.Procedure}\`${row.SourceFiles}`)
}
markdown.push('- 数据库存在但静态前端未调用的过程可能来自事件、定时任务、接口内部调用、历史功能或未授权菜单,不能仅凭“未调用”判定为废弃。')
markdown.push('- 当前测试环境的核心外部依赖、证书、后端高权限账号和写流程验收仍按接管报告中的限制执行。')
markdown.push('')
markdown.push('## 9. 交付清单')
markdown.push('')
markdown.push('- 本用户手册:`doc/MES后台网页版用户操作手册_V1.0_20260725.docx`')
markdown.push('- Markdown 版:`doc/MES后台网页版用户操作手册_V1.0_20260725.md`')
markdown.push('- 菜单清单:`work/menu_inventory_role1.csv`')
markdown.push('- 页面 UI 清单:`work/page_ui_inventory_role1_20260725.csv`')
markdown.push('- 前端过程调用清单:`work/frontend_procedure_call_inventory_20260725.csv`')
markdown.push('- 数据库过程清单:`work/database_procedure_inventory_20260725.csv`')
markdown.push('- 前端/数据库差异清单:`work/frontend_calls_missing_db_procedure_20260725.csv`')
markdown.push('- 全量过程扫描脚本:`work/scan-frontend-function-inventory.js`')
markdown.push('- 页面扫描脚本:`work/scan-page-ui-inventory.js`')
markdown.push('')
markdown.push('> 手册首版基于源码、菜单和存储过程静态盘点;涉及真实业务写入的页面仍需在隔离数据上完成业务负责人验收后,才能标记为生产可用。')
const markdownPath = path.join(docDir, 'MES后台网页版用户操作手册_V1.0_20260725.md')
fs.writeFileSync(markdownPath, markdown.join('\n') + '\n', 'utf8')
const mapLines = []
mapLines.push('# MES 后台网页版功能与存储过程全量盘点')
mapLines.push('')
mapLines.push('盘点日期2026-07-25 | 环境:测试数据库 YL_MESDB | 角色1')
mapLines.push('')
mapLines.push('## 1. 盘点范围')
mapLines.push('')
mapLines.push(`- 前端源码:${scanSummary.sourceFiles} 个文件,其中 ${scanSummary.vueFiles} 个 Vue、${scanSummary.jsFiles} 个 JS。`)
mapLines.push(`- 菜单:${menuRows.length} 条,其中模块 ${rootRows.length} 个、可进入页面 ${pageRows.length}75 个页面全部映射到源码文件。`)
mapLines.push(`- 前端过程调用:${scanSummary.totalCreateDataCalls} 次 CreateData${scanSummary.uniqueLiteralProcedures} 个唯一字面量过程名,${scanSummary.dynamicCreateDataCalls} 次动态调用。`)
mapLines.push(`- 数据库对象:${dbRows.length} 个存储过程、63 张表、27 个视图。`)
mapLines.push(`- 数据库表达式依赖:${dependencyRows.length} 条,其中跨服务器依赖 ${dependencyRows.filter(row => String(row.ReferencedServer || '').trim()).length} 条,跨库依赖 ${dependencyRows.filter(row => String(row.ReferencedDatabase || '').trim()).length} 条。`)
mapLines.push('')
mapLines.push('## 2. 页面到过程映射')
mapLines.push('')
mapLines.push('| 模块 | 页面 | 源码 | 主要操作 | 过程调用 | B1 GET/POST/PATCH | TM GET/POST/PATCH |')
mapLines.push('| --- | --- | --- | --- | --- | --- | --- |')
for (const page of pageRows) mapLines.push(`| ${page.module} | ${page.title} | ${page.source} | ${list(page.buttons, 8)} | ${list(page.procedures.split('|'), 16)} | ${page.b1GetPostPatch} | ${page.tmGetPostPatch} |`)
mapLines.push('')
mapLines.push('## 3. 前端调用与数据库差异')
mapLines.push('')
mapLines.push(`前端字面量调用但测试库找不到同名过程共 **${missingRows.length}** 个。该结果可能由测试库版本差异、动态接口、历史页面或过程名称变更造成,必须逐项确认:`)
mapLines.push('')
for (const row of missingRows) mapLines.push(`- ${row.Procedure}:调用次数 ${row.CallCount},来源 ${row.SourceFiles}`)
mapLines.push('')
mapLines.push(`数据库中有 ${dbRows.filter(row => !frontendRows.some(front => String(front.Procedure || '').toLocaleLowerCase() === String(row.name || '').toLocaleLowerCase())).length} 个过程未被静态前端直接调用。它们可能由事件、定时任务、接口内部调用、其他客户端、历史菜单或后台作业使用,不能仅凭静态未引用判定为废弃。`)
mapLines.push('')
mapLines.push('## 4. 过程风险分类')
mapLines.push('')
mapLines.push(`- 过程总数:${dbRows.length}`)
mapLines.push(`- 定义中出现 INSERT/UPDATE/DELETE/MERGE 等明显写关键字:${dbRows.filter(row => row.HasWriteKeyword === '1').length} 个。`)
mapLines.push(`- 明确包含 OPENQUERY、[SAP] 或现场 SAP 交货表引用:${dbRows.filter(row => row.HasExternalDependency === '1').length} 个。`)
mapLines.push('- 写过程必须按业务数据、权限、事务、回滚和审计要求单独验收;外部依赖过程必须在现场连接可用时再做联调。')
mapLines.push('')
mapLines.push('## 5. 新需求定位路径')
mapLines.push('')
mapLines.push('1. 从菜单路径定位 `src/views/<模块>/<页面>/index.vue`。')
mapLines.push('2. 在页面中搜索 `CreateData`记录过程名、type、参数名和调用前后的页面状态。')
mapLines.push('3. 在 `work/database_procedure_inventory_20260725.csv` 查过程参数数量、写入特征和外部依赖。')
mapLines.push('4. 如果过程不存在,先确认测试库与发布物版本,再决定补过程、改过程名映射还是修复页面。')
mapLines.push('5. 变更必须配套前置校验、事务、验证 SQL、回滚脚本、页面回归和手册更新。')
mapLines.push('')
mapLines.push('## 6. 原始清单')
mapLines.push('')
mapLines.push('- `work/menu_inventory_role1.csv`:角色菜单清单。')
mapLines.push('- `work/page_ui_inventory_role1_20260725.csv`:页面按钮、字段、方法和过程清单。')
mapLines.push('- `work/frontend_procedure_call_inventory_20260725.csv`:前端过程调用清单。')
mapLines.push('- `work/database_procedure_inventory_20260725.csv`:测试库全部过程清单。')
mapLines.push('- `work/database_procedure_parameters_20260725.csv`:全部过程参数清单。')
mapLines.push('- `work/database_table_columns_20260725.csv`、`work/database_view_columns_20260725.csv`:表和视图字段清单。')
mapLines.push('- `work/database_expression_dependencies_20260725.csv`:过程、视图、约束等表达式依赖清单。')
mapLines.push('- `work/frontend_calls_missing_db_procedure_20260725.csv`:前端调用缺失过程清单。')
mapLines.push('- `work/database_procedures_unused_by_static_frontend_20260725.csv`:数据库未被静态前端调用清单。')
const mapPath = path.join(docDir, 'MES后台网页版功能与存储过程全量盘点_20260725.md')
fs.writeFileSync(mapPath, mapLines.join('\n') + '\n', 'utf8')
// A compact DOCX rendering of the same manual. Tables are generated from the same inventory data.
const colors = { navy: '17365D', blue: '1F4E78', lightBlue: 'D9EAF7', gray: '666666', lightGray: 'F2F2F2', green: '548235', paleGreen: 'E2F0D9', amber: 'BF8F00', paleAmber: 'FFF2CC', red: 'C00000', paleRed: 'FCE4D6', border: 'B4C6E7', white: 'FFFFFF' }
const esc = value => String(value == null ? '' : value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;')
function r(text, opts = {}) {
const p = [`<w:rFonts w:ascii="${esc(opts.font || '宋体')}" w:hAnsi="${esc(opts.font || '宋体')}" w:eastAsia="${esc(opts.eastAsia || opts.font || '宋体')}"/>`]
if (opts.bold) p.push('<w:b/>')
if (opts.italic) p.push('<w:i/>')
if (opts.color) p.push(`<w:color w:val="${opts.color}"/>`)
p.push(`<w:sz w:val="${opts.size || 21}"/><w:szCs w:val="${opts.size || 21}"/>`)
return `<w:r><w:rPr>${p.join('')}</w:rPr><w:t xml:space="preserve">${esc(text)}</w:t></w:r>`
}
function p(content, opts = {}) {
const runs = Array.isArray(content) ? content : [r(content, opts.run || {})]
const pp = []
if (opts.style) pp.push(`<w:pStyle w:val="${opts.style}"/>`)
if (opts.align) pp.push(`<w:jc w:val="${opts.align}"/>`)
if (opts.keepNext) pp.push('<w:keepNext/>')
if (opts.pageBreakBefore) pp.push('<w:pageBreakBefore/>')
pp.push(`<w:spacing w:after="${opts.after == null ? 100 : opts.after}" w:line="${opts.line || 320}" w:lineRule="auto"/>`)
return `<w:p><w:pPr>${pp.join('')}</w:pPr>${runs.join('')}</w:p>`
}
function h(text, level, pageBreak = false) { return p(text, { style: `Heading${level}`, keepNext: true, pageBreakBefore: pageBreak }) }
function bulletXml(text) { return p([r('• ', { font: '微软雅黑', eastAsia: '微软雅黑', bold: true, color: colors.blue }), r(text)], { after: 70 }) }
function numberXml(num, text) { return p([r(`${num}. `, { font: '微软雅黑', eastAsia: '微软雅黑', bold: true, color: colors.blue }), r(text)], { after: 80 }) }
function td(text, width, opts = {}) {
const content = Array.isArray(text) ? text : [p(String(text), { run: { size: opts.size || 18, bold: opts.bold, color: opts.color, font: opts.font || '宋体', eastAsia: opts.font || '宋体' }, after: 0, line: 260 })]
return `<w:tc><w:tcPr><w:tcW w:w="${width}" w:type="dxa"/><w:tcMar><w:top w:w="70" w:type="dxa"/><w:left w:w="80" w:type="dxa"/><w:bottom w:w="70" w:type="dxa"/><w:right w:w="80" w:type="dxa"/></w:tcMar><w:vAlign w:val="center"/>${opts.fill ? `<w:shd w:fill="${opts.fill}"/>` : ''}</w:tcPr>${content.join('')}</w:tc>`
}
function tbl(rows, widths, header = false) {
const grid = widths.map(width => `<w:gridCol w:w="${width}"/>`).join('')
const body = rows.map((row, index) => `<w:tr><w:trPr>${header && index === 0 ? '<w:tblHeader/>' : ''}<w:cantSplit/></w:trPr>${row.map((text, i) => td(text, widths[i], { fill: header && index === 0 ? colors.blue : (index % 2 === 0 ? colors.lightBlue : null), bold: header && index === 0, color: header && index === 0 ? colors.white : null })).join('')}</w:tr>`).join('')
return `<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/><w:tblLayout w:type="fixed"/><w:tblBorders><w:top w:val="single" w:sz="5" w:color="${colors.border}"/><w:left w:val="single" w:sz="5" w:color="${colors.border}"/><w:bottom w:val="single" w:sz="5" w:color="${colors.border}"/><w:right w:val="single" w:sz="5" w:color="${colors.border}"/><w:insideH w:val="single" w:sz="3" w:color="D9E2F3"/><w:insideV w:val="single" w:sz="3" w:color="D9E2F3"/></w:tblBorders></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${body}</w:tbl>`
}
function info(title, text, fill = colors.paleGreen, color = colors.green) {
const titleParagraph = p([r(title, { font: '微软雅黑', eastAsia: '微软雅黑', bold: true, color, size: 22 })], { after: 70 })
const textParagraph = p([r(text, { size: 20 })], { after: 0 })
return `<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="5" w:color="${color}"/><w:left w:val="single" w:sz="5" w:color="${color}"/><w:bottom w:val="single" w:sz="5" w:color="${color}"/><w:right w:val="single" w:sz="5" w:color="${color}"/></w:tblBorders></w:tblPr><w:tblGrid><w:gridCol w:w="9000"/></w:tblGrid><w:tr><w:trPr><w:cantSplit/></w:trPr><w:tc><w:tcPr><w:tcW w:w="9000" w:type="dxa"/><w:shd w:fill="${fill}"/><w:tcMar><w:top w:w="90" w:type="dxa"/><w:left w:w="110" w:type="dxa"/><w:bottom w:w="90" w:type="dxa"/><w:right w:w="110" w:type="dxa"/></w:tcMar></w:tcPr>${titleParagraph}${textParagraph}</w:tc></w:tr></w:tbl>`
}
const docBody = []
docBody.push(p('大连元利流体技术有限公司', { align: 'center', after: 100, run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 28, color: colors.gray } }))
docBody.push(p('MES 后台网页版用户操作手册', { align: 'center', after: 150, run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 52, bold: true, color: colors.navy } }))
docBody.push(p('测试环境使用与功能定位指南', { align: 'center', after: 800, run: { font: '微软雅黑', eastAsia: '微软雅黑', size: 26, color: colors.blue } }))
docBody.push(info('手册范围', '本手册覆盖测试角色菜单中的 84 条菜单、75 个页面,提供用户操作、权限、安全边界和问题定位方法。', colors.paleGreen, colors.green))
docBody.push(p('版本V1.0 日期2026-07-25 适用MES 测试环境', { align: 'center', after: 300, run: { size: 20, color: colors.gray } }))
docBody.push(p('本手册不包含账号、密码、Token 或生产业务明细。', { align: 'center', after: 500, run: { size: 18, color: colors.gray } }))
docBody.push('<w:p><w:r><w:br w:type="page"/></w:r></w:p>')
docBody.push(h('1. 使用前说明', 1))
for (const item of ['测试前端地址http://124.220.15.242:10012/。', 'MES 接口由前端自动调用,用户不需要手工拼接接口地址。', '测试环境 B1/TM 只允许登录与只读 GET语义 POST/PATCH 会在前端拦截。', '页面按钮和数据范围由角色权限控制。', '涉及新增、编辑、删除、排产、派工、报工、质检、入库和状态切换的操作,必须先确认业务单据和回滚方式。']) docBody.push(bulletXml(item))
docBody.push(h('2. 登录与通用操作', 1, true))
for (const item of ['打开测试前端地址,输入账号和密码登录。', '登录成功后系统按角色加载菜单和按钮权限。', '记住账号不会保存密码;共享电脑不要勾选。', '列表页面先填写筛选条件再查询,使用分页和导出核对结果。', '保存或状态操作后重新查询,检查状态、数量、操作人和操作时间。']) docBody.push(numberXml(docBody.length, item))
docBody.push(h('3. 菜单页面总览', 1, true))
docBody.push(p(`当前角色菜单 ${menuRows.length} 条,模块 ${rootRows.length} 个,可进入页面 ${pageRows.length} 个。不同角色可能少于此数量。`, { after: 140 }))
for (const rootRow of rootRows) {
const module = rootRow.componet.split('/')[1]
const pages = pagesByModule.get(module) || []
docBody.push(h(rootRow.title, 2))
docBody.push(p(moduleIntros[module] || '用于 MES 业务管理和查询。', { after: 100 }))
const pageTable = [['页面', '路径', '主要操作', '关键字段/内容']]
for (const page of pages) pageTable.push([page.title, page.path, list(page.buttons, 7) || '查询/查看', list(page.columns, 9) || '按页面返回字段'])
docBody.push(tbl(pageTable, [1500, 2600, 2600, 2300], true))
}
docBody.push(h('4. 各模块操作流程', 1, true))
for (const rootRow of rootRows) {
const module = rootRow.componet.split('/')[1]
docBody.push(h(rootRow.title, 2))
for (const item of moduleWorkflow(module)) docBody.push(bulletXml(item))
const pages = pagesByModule.get(module) || []
for (const page of pages) {
docBody.push(h(page.title, 3))
docBody.push(p(`页面:${page.path}`, { after: 50 }))
docBody.push(p(`操作:${list(page.buttons, 12) || '查询/查看'};主要字段:${list(page.columns, 14) || '按接口返回字段展示'}`, { after: 50 }))
if (page.procedures) docBody.push(p(`相关存储过程:${list(page.procedures.split('|'), 14)}`, { after: 50 }))
docBody.push(p('建议:先查询并确认记录,再执行页面按钮;保存或状态操作后重新查询核对结果。', { after: 100 }))
}
}
docBody.push(h('5. 重点业务流程', 1, true))
for (const [title, items] of [
['排产与装配任务', ['按订单、物料、日期和状态查询,再执行排产、派工或优先级操作。', '系统类/阀类装配任务的预算工时手工输入,进度由任务或质检工时除以预算自动计算。', '批量操作后到已排产、工位任务汇总和齐套跟踪结果复核。']],
['工艺维护与下发', ['维护工序和标准工艺,再生成/编辑生产工艺。', '确认工序顺序、准备工时、标准工时、工装量具、机床和车间。', '下发任务前确认订单、工艺版本和目标工序。']],
['加工中心与装配中心', ['按工位、人员、班次和任务状态查询。', '按业务顺序执行收料、开工、暂停/恢复、报工、送检和异常处理。', '操作完成后核对任务状态、工时、数量和最后编辑人。']],
['质量检验与不合格品', ['根据业务节点进入终检、序检、收检、采购入库检、退库检或其他入库检。', '录入检测项目、实测值、判定、合格数和不合格数。', '不合格结果进入不合格品处置并保留操作记录。']],
['人员、角色、菜单和工位', ['先维护人员和角色,再配置菜单权限。', '权限修改后使用目标账号重新登录验证。', '菜单变更前确认路由组件存在。']]
]) { docBody.push(h(title, 2)); items.forEach((item, i) => docBody.push(numberXml(i + 1, item))) }
docBody.push(h('6. 权限、安全与问题定位', 1, true))
for (const item of ['按钮权限来自角色的 AddData、DeleteData、EditData、SearchData 等字段。', '测试环境 B1/TM 语义 POST/PATCH 在网络请求前拦截,不通过放开写权限排查连通性。', '页面问题先定位 src/views/<模块>/<页面>/index.vue再查 CreateData 过程名和参数。', '通用接口入口为 src/utils/request.js、src/utils/curd.js动态菜单入口为 src/router/getTreeData.js。', '数据库问题先检查过程是否存在、参数数量、写入特征和外部依赖,再决定修改前端还是数据库过程。', '不要将密码、Token、生产业务数据或导出文件提交到代码仓库。']) docBody.push(bulletXml(item))
docBody.push(h('7. 全量扫描结果与已知风险', 1, true))
docBody.push(tbl([
['项目', '结果'],
['前端源码', `${scanSummary.sourceFiles} 个源码文件:${scanSummary.vueFiles} 个 Vue、${scanSummary.jsFiles} 个 JS`],
['存储过程调用', `${scanSummary.totalCreateDataCalls} 次 CreateData${scanSummary.uniqueLiteralProcedures} 个唯一字面量过程,${scanSummary.dynamicCreateDataCalls} 次动态调用待人工复核`],
['数据库对象', `${dbRows.length} 个过程、63 张表、27 个视图`],
['前端/数据库差异', `${missingRows.length} 个前端调用在测试库找不到同名过程;另有数据库过程可能来自事件、定时任务或历史功能`],
['外部集成', `B1 GET/POST/PATCH=${scanSummary.b1.get}/${scanSummary.b1.post}/${scanSummary.b1.patch}TM GET/POST/PATCH=${scanSummary.tm.get}/${scanSummary.tm.post}/${scanSummary.tm.patch}`]
], [2600, 6400], true))
if (missingRows.length) {
docBody.push(h('前端调用但测试库缺失的过程', 2))
docBody.push(p('以下过程必须按部署版本、动态调用、历史页面和测试库差异逐项确认,不能直接判定为页面故障:', { after: 80 }))
for (const row of missingRows) docBody.push(bulletXml(`${row.Procedure}${row.SourceFiles}`))
}
docBody.push(h('8. 交付清单', 1, true))
for (const item of ['本用户手册doc/MES后台网页版用户操作手册_V1.0_20260725.docx', 'Markdown 版doc/MES后台网页版用户操作手册_V1.0_20260725.md', '菜单清单work/menu_inventory_role1.csv', '页面 UI 清单work/page_ui_inventory_role1_20260725.csv', '前端过程调用清单work/frontend_procedure_call_inventory_20260725.csv', '数据库过程清单work/database_procedure_inventory_20260725.csv', '前端/数据库差异清单work/frontend_calls_missing_db_procedure_20260725.csv']) docBody.push(bulletXml(item))
docBody.push(info('手册状态', '这是基于源码、菜单和数据库静态盘点生成的 V1.0。涉及真实业务写入的页面,仍需在隔离数据上由业务负责人完成验收后,才能标记为生产可用。', colors.paleAmber, colors.amber))
const section = '<w:sectPr><w:headerReference w:type="default" r:id="rId1"/><w:footerReference w:type="default" r:id="rId2"/><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1360" w:right="1360" w:bottom="1360" w:left="1360" w:header="650" w:footer="650" w:gutter="0"/><w:cols w:space="720"/></w:sectPr>'
const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>${docBody.join('')}${section}</w:body></w:document>`
const stylesXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:eastAsia="宋体"/><w:sz w:val="20"/><w:szCs w:val="20"/><w:lang w:val="zh-CN" w:eastAsia="zh-CN"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="100" w:line="320" w:lineRule="auto"/></w:pPr></w:pPrDefault></w:docDefaults><w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="正文"/><w:rPr><w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:eastAsia="宋体"/><w:sz w:val="20"/><w:szCs w:val="20"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="标题 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="300" w:after="150"/><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑"/><w:b/><w:color w:val="${colors.navy}"/><w:sz w:val="30"/><w:szCs w:val="30"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="标题 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="220" w:after="100"/><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑"/><w:b/><w:color w:val="${colors.blue}"/><w:sz w:val="25"/><w:szCs w:val="25"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="标题 3"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="160" w:after="80"/><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑"/><w:b/><w:color w:val="${colors.gray}"/><w:sz w:val="22"/><w:szCs w:val="22"/></w:rPr></w:style></w:styles>`
const headerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:p><w:pPr><w:jc w:val="right"/></w:pPr>${r('大连元利流体技术有限公司 | MES 后台网页版用户操作手册', { font: '微软雅黑', eastAsia: '微软雅黑', size: 16, color: colors.gray })}</w:p></w:hdr>`
const footerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:p><w:pPr><w:jc w:val="center"/></w:pPr>${r('内部资料 | 第 ', { size: 16, color: colors.gray })}<w:fldSimple w:instr="PAGE"><w:r><w:rPr><w:sz w:val="16"/><w:color w:val="${colors.gray}"/></w:rPr><w:t>1</w:t></w:r></w:fldSimple>${r(' 页', { size: 16, color: colors.gray })}</w:p></w:ftr>`
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/><Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`
const packageRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>`
const documentRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`
const core = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>MES 后台网页版用户操作手册</dc:title><dc:subject>MES 测试环境用户操作与功能定位</dc:subject><dc:creator>MES 接管维护</dc:creator><cp:lastModifiedBy>MES 接管维护</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">2026-07-25T08:00:00Z</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">2026-07-25T08:00:00Z</dcterms:modified></cp:coreProperties>`
const app = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Microsoft Office Word</Application><AppVersion>16.0000</AppVersion><Company>大连元利流体技术有限公司</Company></Properties>`
const zip = new JSZip()
zip.file('[Content_Types].xml', contentTypes)
zip.folder('_rels').file('.rels', packageRels)
zip.folder('docProps').file('core.xml', core).file('app.xml', app)
const word = zip.folder('word')
word.file('document.xml', documentXml).file('styles.xml', stylesXml).file('header1.xml', headerXml).file('footer1.xml', footerXml)
word.folder('_rels').file('document.xml.rels', documentRels)
const docxPath = path.join(docDir, 'MES后台网页版用户操作手册_V1.0_20260725.docx')
fs.writeFileSync(docxPath, zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' }))
console.log(JSON.stringify({ markdownPath, docxPath, mapPath, menuRows: menuRows.length, pages: pageRows.length, dbProcedures: dbRows.length, missingProcedures: missingRows.length, docxBytes: fs.statSync(docxPath).size }))