param([string]$ScriptRootOverride = '') $ErrorActionPreference = 'Stop' $scriptRootPath = if ($ScriptRootOverride) { $ScriptRootOverride } else { $PSScriptRoot } $root = (Resolve-Path (Join-Path $scriptRootPath '..\..')).Path $catalogPath = Join-Path $scriptRootPath 'full-page-catalog.json' $screenshots = Join-Path $scriptRootPath 'full-screenshots' $docDir = Join-Path $root 'doc' $desktop = [Environment]::GetFolderPath('Desktop') $sourceDocx = Join-Path $docDir '大连元利流体技术有限公司MES系统操作手册_系统管理篇_样稿.docx' $fileName = '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23' $docxPath = Join-Path $docDir ($fileName + '.docx') $pdfPath = Join-Path $docDir ($fileName + '.pdf') $desktopDocx = Join-Path $desktop ($fileName + '.docx') $desktopPdf = Join-Path $desktop ($fileName + '.pdf') $progressPath = Join-Path ([IO.Path]::GetTempPath()) 'mes-full-manual-build.log' [IO.File]::WriteAllText($progressPath, "开始生成 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`r`n", [Text.Encoding]::UTF8) function Write-BuildProgress { param([string]$Message) [IO.File]::AppendAllText($progressPath, "$(Get-Date -Format 'HH:mm:ss') $Message`r`n", [Text.Encoding]::UTF8) } if (-not (Test-Path -LiteralPath $sourceDocx)) { throw "系统管理篇源文档不存在:$sourceDocx" } if (-not (Test-Path -LiteralPath $catalogPath)) { throw "页面目录不存在:$catalogPath" } $catalog = Get-Content -Raw -Encoding UTF8 $catalogPath | ConvertFrom-Json if ($catalog.Length -ne 69) { throw "页面目录数量应为69,实际为$($catalog.Length)" } foreach ($page in $catalog) { $imagePath = Join-Path $screenshots $page.screenshot if (-not (Test-Path -LiteralPath $imagePath)) { throw "页面截图不存在:$imagePath" } } New-Item -ItemType Directory -Force -Path $docDir | Out-Null $moduleChapters = [ordered]@{ '设备管理' = 7 '研发管理' = 8 '工艺管理' = 9 '计划排产' = 10 '生产管理' = 11 '异常提醒' = 12 '质量管理' = 13 '销售订单管理' = 14 } function Join-Values { param([object]$Values, [int]$Limit = 12) if ($null -eq $Values -or $Values.Length -eq 0) { return '无' } $items = @($Values | Select-Object -First $Limit) $text = $items -join '、' if ($Values.Length -gt $Limit) { $text += "等$($Values.Length)项" } return $text } function Get-FilterType { param([string]$Field) if ($Field -match '日期|时间|开始|结束|完工|入账') { return '日期/时间或范围选择' } if ($Field -match '状态|类型|类别|属性|性质|结果|部门|工位|工序|人员|指派|来源|班次|齐套|送检|入库|发货') { return '下拉、开关或选项选择' } if ($Field -match '编码|编号|订单|任务ID|计划号|合同号|图号|名称|操作人|物料|产品') { return '文本输入或远程选择' } return '输入或选择控件' } function Get-FilterGuide { param([string]$Field) if ($Field -match '日期|时间|开始|结束|完工|入账') { return '选择起止时间时按区间过滤;清空表示不限制该时间条件。' } if ($Field -match '状态|类型|类别|属性|性质|结果|部门|工位|工序|人员|指派|来源|班次|齐套|送检|入库|发货') { return '从页面提供的选项中选择;“全部”或清空通常表示不限制该条件。' } if ($Field -match '编码|编号|订单|任务ID|计划号|合同号|图号|名称|操作人|物料|产品') { return '输入关键值或从远程候选项中选择,再与其他已填写条件共同查询。' } return '按页面控件输入或选择,点击【查询】后与其他条件共同生效。' } function Get-ActionGuide { param([string]$Button) if ($Button -match '查询|搜索|刷新') { return '读取当前筛选条件并刷新列表,不写入业务数据。' } if ($Button -match '导出|Excel|下载') { return '将当前查询或当前页签结果导出/下载;导出前先确认筛选范围。' } if ($Button -match '查看|预览|明细|记录|图纸|附件') { return '打开当前记录的只读详情、历史、图纸或附件;关闭窗口返回列表。' } if ($Button -match '新增|新建|添加|设备$') { return '打开新增窗口,填写必填项并确认后写入;正式操作前检查编码和关联主数据。' } if ($Button -match '编辑|修改|修正') { return '选择目标记录后打开编辑窗口,核对影响范围并保存修改。' } if ($Button -match '删除|移除|作废|关闭订单') { return '执行不可逆或影响状态的操作;必须核对下游占用并在确认框中再次确认。' } if ($Button -match '取消|关闭$|返回') { return '关闭当前窗口或放弃本次未提交内容,通常不写入数据。' } if ($Button -match '保存|确定|提交|确认') { return '提交当前表单或选择结果;提交前复核必填项、数量、时间和目标记录。' } if ($Button -match '批量|排产|下发|派工|指派') { return '先选择目标行并完整填写工位、人员、数量和时间,再批量生成或下发任务。' } if ($Button -match '开始|开工|暂停|结束|完成|报工|送检|收验|收检|发料|收料|领料|退库') { return '改变任务或物料流转状态并形成操作记录;只对已核对的当前任务执行。' } if ($Button -match '上传') { return '选择文件并上传到当前业务记录;上传前确认文件版本、格式和归属。' } return '按当前选中记录执行对应业务动作;根据弹窗提示核对后再确认。' } function Test-WritePage { param([object]$Page) if ($Page.writeProcedures.Length -gt 0) { return $true } return [bool](@($Page.buttons | Where-Object { $_ -match '新增|新建|编辑|修改|删除|保存|确定|提交|开始|结束|完成|报工|送检|收料|发料|排产|下发|上传' }).Length) } $word = $null $doc = $null $buildError = $null try { $word = New-Object -ComObject Word.Application $word.Visible = $false $word.DisplayAlerts = 0 $doc = $word.Documents.Open($sourceDocx, $false, $false) $doc.SaveAs2($docxPath, 16) Write-BuildProgress '已打开系统管理篇并另存完整篇' $selection = $word.Selection $normal = $doc.Styles.Item(-1) function Set-ParagraphText { param([object]$Paragraph, [string]$Text) $range = $Paragraph.Range.Duplicate if ($range.End -gt $range.Start) { $range.End = $range.End - 1 } $range.Text = $Text } $headingMap = @{ '7 典型业务链路' = '15 典型业务链路' '7.1 新增可登录人员' = '15.1 新增可登录人员' '7.2 下线一个角色' = '15.2 下线一个角色' '7.3 新增生产工位' = '15.3 新增生产工位' '7.4 追溯一次报工操作' = '15.4 追溯一次报工操作' '8 常见问题' = '16 常见问题' '9 正式环境操作红线' = '17 正式环境操作红线' } $paragraphCount = $doc.Paragraphs.Count for ($paragraphIndex = 1; $paragraphIndex -le $paragraphCount; $paragraphIndex++) { $paragraph = $doc.Paragraphs.Item($paragraphIndex) $text = ($paragraph.Range.Text -replace '[\r\a]+$', '').Trim() if ($text -eq '系统管理篇(样稿)') { Set-ParagraphText $paragraph '完整操作手册(系统管理至销售订单)' } elseif ($text -eq '版本:V1.0') { Set-ParagraphText $paragraph '版本:V1.1' } elseif ($text -eq '编制日期:2026年7月23日') { Set-ParagraphText $paragraph '编制日期:2026年7月23日' } elseif ($text.StartsWith('当前正式菜单以数据库配置为准,包括人员管理')) { Set-ParagraphText $paragraph '当前正式菜单以数据库配置为准。本版在系统管理基础上继续覆盖设备管理、研发管理、工艺管理、计划排产、生产管理、异常提醒、质量管理和销售订单管理,共计74个正式功能页面。' } elseif ($headingMap.ContainsKey($text)) { Set-ParagraphText $paragraph $headingMap[$text] } } Write-BuildProgress '已更新封面、阅读说明和后续章节编号' $revisionTable = $null for ($tableIndex = 1; $tableIndex -le $doc.Tables.Count; $tableIndex++) { $candidate = $doc.Tables.Item($tableIndex) if ($candidate.Columns.Count -ge 4) { $firstCell = ($candidate.Cell(1, 1).Range.Text -replace '[\r\a]+$', '').Trim() if ($firstCell -eq '版本') { $revisionTable = $candidate break } } } if ($null -ne $revisionTable) { $newRow = $revisionTable.Rows.Add() $newRow.Cells.Item(1).Range.Text = 'V1.1' $newRow.Cells.Item(2).Range.Text = '2026-07-23' $newRow.Cells.Item(3).Range.Text = '增加设备至销售订单八个模块' $newRow.Cells.Item(4).Range.Text = '新增69个正式功能页、69张页面截图及完整数据关系说明' } Write-BuildProgress '已更新修订记录' $insertAt = $null $paragraphCount = $doc.Paragraphs.Count for ($paragraphIndex = 1; $paragraphIndex -le $paragraphCount; $paragraphIndex++) { $paragraph = $doc.Paragraphs.Item($paragraphIndex) $text = ($paragraph.Range.Text -replace '[\r\a]+$', '').Trim() if ($text -eq '15 典型业务链路') { $insertAt = $paragraph.Range.Start break } } if ($null -eq $insertAt) { throw '未找到“15 典型业务链路”插入位置' } $selection.SetRange($insertAt, $insertAt) Write-BuildProgress '已定位模块章节插入位置' function Move-End { $selection.SetRange($selection.Range.End, $selection.Range.End) } function Add-Text { param( [string]$Text, [double]$Size = 10.5, [bool]$Bold = $false, [int]$Align = 0, [int]$Color = 0, [double]$Before = 0, [double]$After = 6, [double]$FirstIndentCm = 0 ) $selection.Font.NameFarEast = '宋体' $selection.Font.Name = 'Arial' $selection.Font.Size = $Size $selection.Font.Bold = [int]$Bold $selection.Font.Color = $Color $selection.ParagraphFormat.Alignment = $Align $selection.ParagraphFormat.SpaceBefore = $Before $selection.ParagraphFormat.SpaceAfter = $After $selection.ParagraphFormat.FirstLineIndent = $word.CentimetersToPoints($FirstIndentCm) $selection.TypeText($Text) $selection.TypeParagraph() } function Add-Heading { param([string]$Text, [int]$Level) $styleId = switch ($Level) { 1 { -2 } 2 { -3 } default { -4 } } $selection.Style = $doc.Styles.Item($styleId) $selection.Font.NameFarEast = '微软雅黑' $selection.Font.Name = 'Arial' $selection.Font.Color = 0x8F4A31 $selection.Font.Bold = 1 $selection.ParagraphFormat.KeepWithNext = -1 $selection.TypeText($Text) $selection.TypeParagraph() $selection.Style = $normal } function Add-Table { param([object[]]$Rows, [double[]]$WidthsCm = @()) if ($Rows.Count -eq 0) { return } $range = $selection.Range $table = $doc.Tables.Add($range, $Rows.Count, $Rows[0].Count) $table.Borders.Enable = 1 $table.Range.Font.NameFarEast = '宋体' $table.Range.Font.Name = 'Arial' $table.Range.Font.Size = 8.5 $table.Range.ParagraphFormat.SpaceAfter = 0 for ($r = 0; $r -lt $Rows.Count; $r++) { for ($c = 0; $c -lt $Rows[$r].Count; $c++) { $table.Cell($r + 1, $c + 1).Range.Text = [string]$Rows[$r][$c] $table.Cell($r + 1, $c + 1).VerticalAlignment = 1 if ($WidthsCm.Count -gt $c) { $table.Cell($r + 1, $c + 1).Width = $word.CentimetersToPoints($WidthsCm[$c]) } } } $table.Rows.Item(1).Range.Bold = 1 $table.Rows.Item(1).Shading.BackgroundPatternColor = 0xE6D8D0 $table.Rows.Item(1).Range.ParagraphFormat.Alignment = 1 $table.AllowAutoFit = -1 $selection.SetRange($table.Range.End, $table.Range.End) $selection.TypeParagraph() } function Add-Note { param([string]$Title, [string]$Text, [string]$Kind = 'info') $range = $selection.Range $table = $doc.Tables.Add($range, 1, 1) $table.Borders.Enable = 1 $cell = $table.Cell(1, 1) $cell.Range.Text = $Title + ':' + $Text $cell.Range.Font.NameFarEast = '微软雅黑' $cell.Range.Font.Name = 'Arial' $cell.Range.Font.Size = 9.5 $cell.Range.ParagraphFormat.SpaceAfter = 0 if ($Kind -eq 'warn') { $cell.Shading.BackgroundPatternColor = 0xD9F2FF } elseif ($Kind -eq 'danger') { $cell.Shading.BackgroundPatternColor = 0xD9E2F3 } else { $cell.Shading.BackgroundPatternColor = 0xF4F1EE } $selection.SetRange($table.Range.End, $table.Range.End) $selection.TypeParagraph() } function Add-Figure { param([string]$Path, [string]$Caption, [double]$MaxWidthCm = 16.4) $selection.ParagraphFormat.Alignment = 1 $shape = $selection.InlineShapes.AddPicture($Path, $false, $true, $selection.Range) $shape.LockAspectRatio = -1 $maxWidth = $word.CentimetersToPoints($MaxWidthCm) if ($shape.Width -gt $maxWidth) { $shape.Width = $maxWidth } $selection.SetRange($shape.Range.End, $shape.Range.End) $selection.TypeParagraph() Add-Text $Caption 9 $false 1 0x777777 0 8 0 } function Add-PageBreak { $selection.InsertBreak(7) } Add-PageBreak foreach ($moduleName in $moduleChapters.Keys) { $chapter = $moduleChapters[$moduleName] $modulePages = @($catalog | Where-Object { $_.module -eq $moduleName } | Sort-Object pageIndex) if ($modulePages.Count -eq 0) { continue } Write-BuildProgress "开始生成第$chapter章 $moduleName" Add-Heading ("$chapter $moduleName") 1 Add-Text $modulePages[0].moduleRelation 10.5 $false 0 0 0 6 0.74 Add-Note '本章范围' "本章按正式菜单顺序说明$($modulePages.Count)个功能页。截图仅执行登录、页面导航和默认查询,没有点击任何业务写入按钮。" 'warn' $moduleRows = [System.Collections.Generic.List[object]]::new() $moduleRows.Add(@('序号', '功能页面', '主要用途')) for ($i = 0; $i -lt $modulePages.Count; $i++) { $moduleRows.Add(@(($i + 1).ToString(), $modulePages[$i].title, $modulePages[$i].purpose)) } Add-Table $moduleRows.ToArray() @(1.2, 3.4, 10.8) for ($pageIndex = 0; $pageIndex -lt $modulePages.Count; $pageIndex++) { $page = $modulePages[$pageIndex] $section = "$chapter.$($pageIndex + 1)" Write-BuildProgress "生成 $section $($page.title)" Add-PageBreak Add-Heading ("$section $($page.title)") 2 Add-Text $page.purpose 10.5 $false 0 0 0 6 0.74 $writePage = Test-WritePage $page Add-Table @( @('项目', '说明'), @('菜单路径', "$moduleName > $($page.title)"), @('页面路由', $page.route), @('数据性质', $(if ($writePage) { '查询为只读;业务按钮可能新增、修改或改变状态' } else { '以只读查询、汇总或导出为主' })) ) @(3.2, 12.2) Add-Figure (Join-Path $screenshots $page.screenshot) ("图 $section-1 $moduleName - $($page.title)正式页面") Add-Heading ("$section.1 搜索与过滤") 3 if ($page.searchFields.Length -gt 0) { $filterRows = [System.Collections.Generic.List[object]]::new() $filterRows.Add(@('条件', '控件形式', '使用说明')) foreach ($field in $page.searchFields) { $filterRows.Add(@($field, (Get-FilterType $field), (Get-FilterGuide $field))) } Add-Table $filterRows.ToArray() @(3.2, 4.2, 8.2) Add-Note '组合规则' '多个条件同时填写时,由页面一并传入后台查询,通常取条件交集。若查询不到记录,先逐项清空状态、日期和编号条件,再重新查询;精确或包含匹配以对应页面后台过程为准。' 'info' } else { Add-Text '本页面没有独立的顶部搜索区,进入页面后按默认条件加载数据,或通过页签、树节点、当前工位/人员上下文切换数据范围。' 10.5 $false 0 0 0 6 0.74 } if ($page.tabs.Length -gt 0) { Add-Table @( @('页面页签/视图', '切换说明'), @((Join-Values $page.tabs 20), '切换页签会改变展示维度或数据范围;导出前确认当前页签。') ) @(5.2, 10.4) } Add-Heading ("$section.2 列表字段与页面结果") 3 if ($page.tableColumns.Length -gt 0) { $columnRows = [System.Collections.Generic.List[object]]::new() $columnRows.Add(@('分组', '字段/列')) for ($columnIndex = 0; $columnIndex -lt $page.tableColumns.Length; $columnIndex += 8) { $endIndex = [Math]::Min($columnIndex + 7, $page.tableColumns.Length - 1) $group = @($page.tableColumns[$columnIndex..$endIndex]) $columnRows.Add(@("第$($columnIndex + 1)-$($endIndex + 1)列", ($group -join '、'))) } Add-Table $columnRows.ToArray() @(3.2, 12.4) Add-Text '列表用于展示当前筛选结果。存在横向滚动条时,应左右滚动核对全部状态、数量、时间和操作列;存在分页时,页码和每页条数只影响当前显示范围。' 10.5 $false 0 0 0 6 0.74 } else { Add-Text '该页面主要使用自定义看板、卡片或复用组件展示结果,字段布局以截图及页面实时内容为准。' 10.5 $false 0 0 0 6 0.74 } Add-Heading ("$section.3 功能按钮与操作") 3 if ($page.buttons.Length -gt 0) { $buttonRows = [System.Collections.Generic.List[object]]::new() $buttonRows.Add(@('按钮/命令', '操作说明')) foreach ($button in $page.buttons) { $buttonRows.Add(@($button, (Get-ActionGuide $button))) } Add-Table $buttonRows.ToArray() @(4.2, 11.4) } else { Add-Text '本页主要通过大尺寸现场操作按钮、图标按钮、表格行操作或复用组件执行命令。操作前必须先选择当前任务,并根据按钮文字和确认提示核对目标记录。' 10.5 $false 0 0 0 6 0.74 } if ($writePage) { Add-Note '正式操作注意' '新增、编辑、删除、开始、结束、排产、报工、送检、收料、上传等操作会写入业务数据或改变状态。本文档编制未实际执行这些按钮;正式使用时应按岗位权限和业务单据逐条确认。' 'danger' } Add-Heading ("$section.4 前后数据关系") 3 Add-Text $page.moduleRelation 10.5 $false 0 0 0 6 0.74 Add-Table @( @('关系层次', '内容'), @('页面输入', $(if ($page.searchFields.Length -gt 0) { Join-Values $page.searchFields 18 } else { '当前登录人员、角色、工位、默认状态或上游任务上下文' })), @('只读查询过程/接口', (Join-Values $page.readProcedures 10)), @('写入过程/接口', (Join-Values $page.writeProcedures 10)), @('页面输出', $(if ($page.tableColumns.Length -gt 0) { Join-Values $page.tableColumns 16 } else { '自定义看板、统计结果、状态卡片或复用组件结果' })) ) @(4.2, 11.4) Add-Note '技术核对说明' '过程名称来自当前前端源码和已引用接口文件,用于说明数据流向;动态过程名、通用审批接口及服务端二次调用可能不会全部显示在本表中。' 'info' } Write-BuildProgress "完成第$chapter章 $moduleName" } Write-BuildProgress '开始更新目录并导出PDF' if ($doc.TablesOfContents.Count -gt 0) { $doc.TablesOfContents.Item(1).Update() } $doc.Save() Write-BuildProgress 'DOCX正文及目录已保存' $doc.ExportAsFixedFormat($pdfPath, 17) Write-BuildProgress 'PDF已导出' # WPS 兼容层在大型文档导出后读取统计信息可能长时间阻塞,统计校验改由生成后的 Open XML/PDF 文件完成。 $doc.Close($false) [void][Runtime.InteropServices.Marshal]::ReleaseComObject($doc) $doc = $null $word.Quit() [void][Runtime.InteropServices.Marshal]::ReleaseComObject($word) $word = $null Copy-Item -LiteralPath $docxPath -Destination $desktopDocx -Force Copy-Item -LiteralPath $pdfPath -Destination $desktopPdf -Force Write-Output "DOCX=$docxPath" Write-Output "PDF=$pdfPath" Write-BuildProgress '完整操作手册生成完成' } catch { $buildError = $_ Write-BuildProgress ("生成失败:" + $_.Exception.Message) } finally { if ($doc) { try { $doc.Close($false) } catch { } } if ($word) { try { $word.Quit() } catch { } } if ($doc) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($doc) } if ($word) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($word) } [GC]::Collect() [GC]::WaitForPendingFinalizers() } if ($buildError) { throw $buildError }