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(/ /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, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') function r(text, opts = {}) { const p = [``] if (opts.bold) p.push('') if (opts.italic) p.push('') if (opts.color) p.push(``) p.push(``) return `${p.join('')}${esc(text)}` } function p(content, opts = {}) { const runs = Array.isArray(content) ? content : [r(content, opts.run || {})] const pp = [] if (opts.style) pp.push(``) if (opts.align) pp.push(``) if (opts.keepNext) pp.push('') if (opts.pageBreakBefore) pp.push('') pp.push(``) return `${pp.join('')}${runs.join('')}` } 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 `${opts.fill ? `` : ''}${content.join('')}` } function tbl(rows, widths, header = false) { const grid = widths.map(width => ``).join('') const body = rows.map((row, index) => `${header && index === 0 ? '' : ''}${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('')}`).join('') return `${grid}${body}` } 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 `${titleParagraph}${textParagraph}` } 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('') 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 = '' const documentXml = `${docBody.join('')}${section}` const stylesXml = `` const headerXml = `${r('大连元利流体技术有限公司 | MES 后台网页版用户操作手册', { font: '微软雅黑', eastAsia: '微软雅黑', size: 16, color: colors.gray })}` const footerXml = `${r('内部资料 | 第 ', { size: 16, color: colors.gray })}1${r(' 页', { size: 16, color: colors.gray })}` const contentTypes = `` const packageRels = `` const documentRels = `` const core = `MES 后台网页版用户操作手册MES 测试环境用户操作与功能定位MES 接管维护MES 接管维护2026-07-25T08:00:00Z2026-07-25T08:00:00Z` const app = `Microsoft Office Word16.0000大连元利流体技术有限公司` 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 }))