generate_s4_blueprint_docx.js 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902
  1. /**
  2. * Generate S4 采购执行模块蓝图设计方案.docx
  3. * 参照 generate_s1_blueprint_docx.js 模板格式
  4. * 章节结构: 封面 → 文档控制 → TOC → Ch1 总体方案 → Ch2~7 功能域(各8子节) → Ch8 数据中台 → Ch9 权限汇总
  5. */
  6. const fs = require('fs');
  7. const sharp = require('sharp');
  8. const {
  9. Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
  10. HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType,
  11. convertInchesToTwip, PageBreak, ImageRun, TabStopType, TabStopPosition,
  12. TableOfContents
  13. } = require('docx');
  14. // ═══════════════════════════════════════════════════
  15. // 通用样式与辅助函数 (from S1 template)
  16. // ═══════════════════════════════════════════════════
  17. const FONT = '微软雅黑';
  18. const thinBorder = {
  19. top: { style: BorderStyle.SINGLE, size: 1 },
  20. bottom: { style: BorderStyle.SINGLE, size: 1 },
  21. left: { style: BorderStyle.SINGLE, size: 1 },
  22. right: { style: BorderStyle.SINGLE, size: 1 },
  23. };
  24. const headerShading = { fill: '4472C4', type: ShadingType.CLEAR };
  25. function p(text, opts = {}) {
  26. return new Paragraph({
  27. children: [new TextRun({ text, size: 21, font: FONT, ...opts })],
  28. spacing: { after: 120, line: 360 },
  29. });
  30. }
  31. function bp(text) { return p(text, { bold: true }); }
  32. function empty() { return new Paragraph({ spacing: { after: 80 }, children: [] }); }
  33. function pageBreak() { return new Paragraph({ children: [new PageBreak()] }); }
  34. function h1(text) {
  35. return new Paragraph({
  36. children: [new TextRun({ text, bold: true, size: 32, font: FONT })],
  37. heading: HeadingLevel.HEADING_1,
  38. spacing: { before: 400, after: 200 },
  39. });
  40. }
  41. function h2(text) {
  42. return new Paragraph({
  43. children: [new TextRun({ text, bold: true, size: 28, font: FONT })],
  44. heading: HeadingLevel.HEADING_2,
  45. spacing: { before: 300, after: 150 },
  46. });
  47. }
  48. function h3(text) {
  49. return new Paragraph({
  50. children: [new TextRun({ text, bold: true, size: 24, font: FONT })],
  51. heading: HeadingLevel.HEADING_3,
  52. spacing: { before: 200, after: 100 },
  53. });
  54. }
  55. function bullet(text, indent = 0) {
  56. return new Paragraph({
  57. children: [new TextRun({ text: ` ${text}`, size: 21, font: FONT })],
  58. spacing: { after: 60, line: 340 },
  59. indent: { left: 300 + indent * 400 },
  60. });
  61. }
  62. // Table helpers
  63. function hdrCell(text, w = 1800) {
  64. return new TableCell({
  65. children: [new Paragraph({ children: [new TextRun({ text, bold: true, size: 20, font: FONT, color: 'FFFFFF' })], alignment: AlignmentType.CENTER })],
  66. width: { size: w, type: WidthType.DXA }, borders: thinBorder, shading: headerShading,
  67. });
  68. }
  69. function tc(text, w = 1800, opts = {}) {
  70. return new TableCell({
  71. children: [new Paragraph({ children: [new TextRun({ text: String(text ?? ''), size: 20, font: FONT })], alignment: opts.center ? AlignmentType.CENTER : AlignmentType.LEFT })],
  72. width: { size: w, type: WidthType.DXA }, borders: thinBorder,
  73. });
  74. }
  75. function table(headers, rows, widths) {
  76. return new Table({
  77. rows: [
  78. new TableRow({ children: headers.map((h, i) => hdrCell(h, widths ? widths[i] : 1800)), tableHeader: true }),
  79. ...rows.map(r => new TableRow({ children: r.map((c, i) => tc(c, widths ? widths[i] : 1800)) })),
  80. ],
  81. width: { size: 100, type: WidthType.PERCENTAGE },
  82. });
  83. }
  84. function activityTable(rows) {
  85. return table(['编号', '活动名称', '执行角色', '活动描述', '输入', '输出'], rows, [600, 1400, 1100, 3800, 1400, 1700]);
  86. }
  87. function ruleTable(rows) {
  88. return table(['序列号', '业务情形', '描述/方案'], rows, [900, 2800, 6300]);
  89. }
  90. function interfaceTable(rows) {
  91. return table(['序号', '接口名称', '接口说明', '频次及触发方式'], rows, [600, 2400, 4200, 2800]);
  92. }
  93. function reportTable(rows) {
  94. return table(['序号', '名称', '描述', '方案'], rows, [600, 2600, 3600, 3200]);
  95. }
  96. function permTable(rows) {
  97. return table(['序号', '岗位名称', '对应系统权限'], rows, [600, 2000, 7400]);
  98. }
  99. // ═══════════════════════════════════════════════════
  100. // 目录 (TOC)
  101. // ═══════════════════════════════════════════════════
  102. const TOC2_TAB = 2880;
  103. const TOC3_TAB = 3600;
  104. const TOC_FONT_SIZE = 20;
  105. function tocChapter(num, title) {
  106. return new Paragraph({
  107. children: [
  108. new TextRun({ text: String(num), size: TOC_FONT_SIZE, font: FONT }),
  109. new TextRun({ text: '\t', size: TOC_FONT_SIZE, font: FONT }),
  110. new TextRun({ text: title, bold: true, size: TOC_FONT_SIZE, font: FONT }),
  111. new TextRun({ text: '\t', size: TOC_FONT_SIZE, font: FONT }),
  112. new TextRun({ text: ' ', size: TOC_FONT_SIZE, font: FONT }),
  113. ],
  114. tabStops: [
  115. { type: TabStopType.LEFT, position: TOC2_TAB },
  116. { type: TabStopType.RIGHT, position: TabStopPosition.MAX },
  117. ],
  118. spacing: { before: 80, after: 40 },
  119. });
  120. }
  121. function tocSub(num, title) {
  122. return new Paragraph({
  123. children: [
  124. new TextRun({ text: String(num), size: TOC_FONT_SIZE, font: FONT }),
  125. new TextRun({ text: '\t', size: TOC_FONT_SIZE, font: FONT }),
  126. new TextRun({ text: title, size: TOC_FONT_SIZE, font: FONT }),
  127. new TextRun({ text: '\t', size: TOC_FONT_SIZE, font: FONT }),
  128. new TextRun({ text: ' ', size: TOC_FONT_SIZE, font: FONT }),
  129. ],
  130. tabStops: [
  131. { type: TabStopType.LEFT, position: TOC3_TAB },
  132. { type: TabStopType.RIGHT, position: TabStopPosition.MAX },
  133. ],
  134. spacing: { before: 30, after: 30 },
  135. });
  136. }
  137. function buildTOC() {
  138. const ch = [];
  139. ch.push(new TableOfContents('目录', { headingStyleRange: '1-2', hyperlink: true }));
  140. ch.push(new Paragraph({
  141. children: [new TextRun({ text: '(如目录未显示,请在 Word 中右键此处 → 更新域)', size: 18, font: FONT, italics: true, color: '888888' })],
  142. spacing: { before: 40, after: 200 },
  143. }));
  144. ch.push(tocChapter('', '文档控制'));
  145. ch.push(tocSub('', '更改记录'));
  146. ch.push(tocSub('', '审核'));
  147. ch.push(tocSub('', '发布'));
  148. ch.push(tocChapter('', '目录'));
  149. ch.push(tocChapter('1', '总体业务方案'));
  150. ch.push(tocSub('1.1', '目标和宗旨'));
  151. ch.push(tocSub('1.2', '总体业务流程图'));
  152. ch.push(tocSub('1.3', '方案设计'));
  153. const chSubs = [
  154. '目标/宗旨', '业务流程图', '业务流程说明', '业务流程规则',
  155. '变化和改进点', '权限管理需求', '系统接口集成', '报表需求',
  156. ];
  157. const chTitles = [
  158. '供应商交货管理', '供应商发货单', '采购退货单', 'IQC退货查询',
  159. '采购执行看板主页', '供应商欠料看板',
  160. ];
  161. for (let i = 0; i < chTitles.length; i++) {
  162. const n = i + 2;
  163. ch.push(tocChapter(String(n), chTitles[i]));
  164. for (let j = 0; j < chSubs.length; j++) {
  165. ch.push(tocSub(`${n}.${j + 1}`, chSubs[j]));
  166. }
  167. }
  168. ch.push(tocChapter('8', '数据中台与MDP集成'));
  169. ['目标/宗旨', '分层设计', 'S4核心数据流', '数据表清单', 'MDP作业调度'].forEach((title, i) => {
  170. ch.push(tocSub(`8.${i + 1}`, title));
  171. });
  172. ch.push(tocChapter('9', '权限管理汇总'));
  173. return ch;
  174. }
  175. // ═══════════════════════════════════════════════════
  176. // 流程图生成 (S1 style: 纵向左角色+右节点)
  177. // ═══════════════════════════════════════════════════
  178. const FLOW_W = 680, BOX_W = 220, BOX_H = 42, GAP_Y = 28;
  179. const ROLE_X = 30, BOX_X = 130, MARGIN_Y = 40;
  180. const flowImageCache = {};
  181. function flowImageParagraph(pngBuffer) {
  182. return new Paragraph({
  183. children: [new ImageRun({ data: pngBuffer, transformation: { width: 540, height: 360 }, type: 'png' })],
  184. alignment: AlignmentType.CENTER,
  185. spacing: { before: 120, after: 120 },
  186. });
  187. }
  188. async function generateFlowPNG(steps) {
  189. const N = steps.length;
  190. const svgH = MARGIN_Y * 2 + N * (BOX_H + GAP_Y) - GAP_Y;
  191. const centerX = BOX_X + BOX_W / 2;
  192. let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${FLOW_W}" height="${svgH}">`;
  193. svg += `<rect width="100%" height="100%" fill="#FAFBFC"/>`;
  194. steps.forEach((s, i) => {
  195. const y = MARGIN_Y + i * (BOX_H + GAP_Y);
  196. svg += `<text x="${ROLE_X}" y="${y + BOX_H / 2 + 5}" font-family="Microsoft YaHei" font-size="12" fill="#666" text-anchor="start">${s.role || ''}</text>`;
  197. const isStart = i === 0, isEnd = i === N - 1;
  198. const fill = isStart ? '#1565C0' : (isEnd ? '#2E7D32' : '#E3F2FD');
  199. const stroke = isStart ? '#0D47A1' : (isEnd ? '#1B5E20' : '#1565C0');
  200. const textColor = (isStart || isEnd) ? '#FFFFFF' : '#333333';
  201. svg += `<rect x="${BOX_X}" y="${y}" width="${BOX_W}" height="${BOX_H}" rx="6" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>`;
  202. svg += `<text x="${centerX}" y="${y + BOX_H / 2 + 5}" font-family="Microsoft YaHei" font-size="13" fill="${textColor}" text-anchor="middle">${s.label}</text>`;
  203. if (i < N - 1) {
  204. const ay = y + BOX_H + 4, by2 = ay + GAP_Y - 8;
  205. svg += `<line x1="${centerX}" y1="${ay}" x2="${centerX}" y2="${by2}" stroke="#888" stroke-width="1.5"/>`;
  206. svg += `<polygon points="${centerX - 5},${by2 - 10} ${centerX + 5},${by2 - 10} ${centerX},${by2}" fill="#888"/>`;
  207. }
  208. });
  209. svg += '</svg>';
  210. return sharp(Buffer.from(svg)).png().toBuffer();
  211. }
  212. // 第1章专用:S4 总体业务流程图(交货执行 → 退货闭环 → 看板监控)
  213. async function generateOverviewFlowPNG() {
  214. const W = 780, H = 540;
  215. let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">`;
  216. svg += `<rect width="100%" height="100%" fill="#FAFBFC"/>`;
  217. svg += `<text x="${W/2}" y="28" font-family="Microsoft YaHei" font-size="15" font-weight="bold" fill="#1F4E79" text-anchor="middle">S4 采购执行 — 总体业务流程</text>`;
  218. const zones = [
  219. { y: 42, h: 160, color: '#E8F0FE', label: '交货执行域(计划发布→交货计划发布→交期回复→生成发货单→在途跟踪)', lx: 10, ly: 60 },
  220. { y: 210, h: 130, color: '#FFF3E0', label: '退货闭环域(IQC检验→判定→退货出库)', lx: 10, ly: 228 },
  221. { y: 348, h: 140, color: '#E8F5E9', label: '采购执行看板域(KPI监控→欠料预测→数据中台)', lx: 10, ly: 366 },
  222. ];
  223. zones.forEach(z => {
  224. svg += `<rect x="8" y="${z.y}" width="${W-16}" height="${z.h}" rx="6" fill="${z.color}" stroke="#ccc" stroke-width="0.5" stroke-dasharray="4,3"/>`;
  225. svg += `<text x="${z.lx}" y="${z.ly}" font-family="Microsoft YaHei" font-size="12" font-weight="bold" fill="#555">${z.label}</text>`;
  226. });
  227. // 交货执行域节点
  228. const deNodes = [
  229. { x: 30, y: 85, w: 130, label: '物料交货\n计划发布', color: '#1565C0' },
  230. { x: 185, y: 85, w: 130, label: '供应商交货\n计划发布', color: '#1565C0' },
  231. { x: 340, y: 85, w: 120, label: '供应商\n交期回复', color: '#1565C0' },
  232. { x: 485, y: 85, w: 120, label: '供应商\n生成发货单', color: '#2E7D32' },
  233. { x: 330, y: 145, w: 130, label: '在途/在检\n入库跟踪', color: '#2E7D32' },
  234. ];
  235. deNodes.forEach(n => {
  236. const fill = n.color;
  237. const stroke = n.color;
  238. const tc = '#fff';
  239. svg += `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="36" rx="5" fill="${fill}" stroke="${stroke}" stroke-width="1.2"/>`;
  240. const lines = n.label.split('\n');
  241. const cy = n.y + 18;
  242. lines.forEach((l, li) => {
  243. const off = (lines.length - 1) * -7 + li * 14;
  244. svg += `<text x="${n.x + n.w/2}" y="${cy + off + 4}" font-family="Microsoft YaHei" font-size="10" fill="${tc}" text-anchor="middle">${l}</text>`;
  245. });
  246. });
  247. svg += `<line x1="160" y1="103" x2="183" y2="103" stroke="#666" stroke-width="1.2"/>`;
  248. svg += `<polygon points="183,98 183,108 190,103" fill="#666"/>`;
  249. svg += `<line x1="315" y1="103" x2="338" y2="103" stroke="#666" stroke-width="1.2"/>`;
  250. svg += `<polygon points="338,98 338,108 345,103" fill="#666"/>`;
  251. svg += `<line x1="460" y1="103" x2="483" y2="103" stroke="#666" stroke-width="1.2"/>`;
  252. svg += `<polygon points="483,98 483,108 490,103" fill="#666"/>`;
  253. svg += `<line x1="545" y1="121" x2="395" y2="145" stroke="#666" stroke-width="1.2"/>`;
  254. svg += `<polygon points="399,140 399,150 392,145" fill="#666"/>`;
  255. // 退货闭环域节点
  256. const rnNodes = [
  257. { x: 30, y: 250, w: 130, label: 'IQC来料检验', color: '#E65100' },
  258. { x: 200, y: 250, w: 130, label: '不合格判定\n(退货/让步/挑选)', color: '#E65100' },
  259. { x: 370, y: 250, w: 130, label: '退货单创建', color: '#B71C1C' },
  260. { x: 540, y: 250, w: 130, label: '退货出库', color: '#B71C1C' },
  261. ];
  262. rnNodes.forEach(n => {
  263. const isRed = n.color === '#B71C1C';
  264. const fill = isRed ? '#FFEBEE' : n.color;
  265. const stroke = isRed ? '#C62828' : n.color;
  266. const tc = isRed ? '#333' : '#fff';
  267. svg += `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="42" rx="5" fill="${fill}" stroke="${stroke}" stroke-width="1.2"/>`;
  268. const lines = n.label.split('\n');
  269. const cy = n.y + 21;
  270. lines.forEach((l, li) => {
  271. const off = (lines.length - 1) * -7 + li * 14;
  272. svg += `<text x="${n.x + n.w/2}" y="${cy + off + 4}" font-family="Microsoft YaHei" font-size="10" fill="${tc}" text-anchor="middle">${l}</text>`;
  273. });
  274. });
  275. svg += `<line x1="160" y1="271" x2="200" y2="271" stroke="#666" stroke-width="1.2"/>`;
  276. svg += `<polygon points="196,266 196,276 203,271" fill="#666"/>`;
  277. svg += `<line x1="330" y1="271" x2="370" y2="271" stroke="#666" stroke-width="1.2"/>`;
  278. svg += `<polygon points="366,266 366,276 373,271" fill="#666"/>`;
  279. svg += `<line x1="500" y1="271" x2="540" y2="271" stroke="#666" stroke-width="1.2"/>`;
  280. svg += `<polygon points="536,266 536,276 543,271" fill="#666"/>`;
  281. // 看板域节点
  282. const kbNodes = [
  283. { x: 30, y: 395, w: 150, label: 'MDP数据同步\n(KPI指标计算引擎)', color: '#2E7D32' },
  284. { x: 220, y: 395, w: 130, label: '采购执行看板\n(双面板布局)', color: '#2E7D32' },
  285. { x: 390, y: 395, w: 130, label: '供应商欠料看板\n(15天滚动预测)', color: '#0D47A1' },
  286. { x: 560, y: 395, w: 130, label: '导出报告\n+运营指标建模', color: '#B71C1C' },
  287. ];
  288. kbNodes.forEach(n => {
  289. const isRed = n.color === '#B71C1C';
  290. const fill = isRed ? '#FFEBEE' : n.color;
  291. const stroke = isRed ? '#C62828' : n.color;
  292. const tc = (n.color === '#2E7D32' || n.color === '#0D47A1') ? '#fff' : '#333';
  293. svg += `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="48" rx="5" fill="${fill}" stroke="${stroke}" stroke-width="1.2"/>`;
  294. const lines = n.label.split('\n');
  295. const cy = n.y + 24;
  296. lines.forEach((l, li) => {
  297. const off = (lines.length - 1) * -7 + li * 14;
  298. svg += `<text x="${n.x + n.w/2}" y="${cy + off + 4}" font-family="Microsoft YaHei" font-size="10" fill="${tc}" text-anchor="middle">${l}</text>`;
  299. });
  300. });
  301. svg += `<line x1="180" y1="419" x2="220" y2="419" stroke="#666" stroke-width="1.2"/>`;
  302. svg += `<polygon points="216,414 216,424 223,419" fill="#666"/>`;
  303. svg += `<line x1="350" y1="419" x2="390" y2="419" stroke="#666" stroke-width="1.2"/>`;
  304. svg += `<polygon points="386,414 386,424 393,419" fill="#666"/>`;
  305. svg += `<line x1="520" y1="419" x2="560" y2="419" stroke="#666" stroke-width="1.2"/>`;
  306. svg += `<polygon points="556,414 556,424 563,419" fill="#666"/>`;
  307. // 串联标注
  308. svg += `<text x="150" y="202" font-family="Microsoft YaHei" font-size="10" fill="#888" text-anchor="middle">↓ 交货数据(含发货单)作为采购执行绩效数据源 ↓</text>`;
  309. svg += `<text x="150" y="340" font-family="Microsoft YaHei" font-size="10" fill="#888" text-anchor="middle">↓ 退货单数据反馈至供应商绩效评估 ↓</text>`;
  310. svg += `<text x="50" y="498" font-family="Microsoft YaHei" font-size="9" fill="#999">数据链路: ERP/SRM/MES → MDP入站 → ODS(STG) → STD → DWD宽表 → KPI计算 → 看板消费</text>`;
  311. svg += '</svg>';
  312. return sharp(Buffer.from(svg)).png().toBuffer();
  313. }
  314. // ═══════════════════════════════════════════════════
  315. // 章节构建函数:8 子章节
  316. // ═══════════════════════════════════════════════════
  317. function buildChapter(num, title, cfg) {
  318. const ch = [];
  319. const pre = `${num}.`;
  320. ch.push(h2(`${pre}1 目标/宗旨`));
  321. (cfg.targets || []).forEach(t => ch.push(bullet(t)));
  322. ch.push(h2(`${pre}2 业务流程图`));
  323. const img = cfg.flowImage || (cfg.flowSteps ? flowImageCache[num] : null);
  324. if (img) {
  325. ch.push(flowImageParagraph(img));
  326. } else {
  327. ch.push(p('(见系统操作流程图 / 附件 Visio 流程图)'));
  328. }
  329. if (cfg.flowNote) ch.push(p(cfg.flowNote));
  330. ch.push(h2(`${pre}3 业务流程说明`));
  331. if (cfg.activities && cfg.activities.length) {
  332. ch.push(activityTable(cfg.activities));
  333. } else {
  334. ch.push(p('参照系统页面操作流程,主要操作步骤包含:'));
  335. (cfg.activityDesc || []).forEach(a => ch.push(bullet(a)));
  336. }
  337. ch.push(h2(`${pre}4 业务流程规则`));
  338. if (cfg.rules && cfg.rules.length) {
  339. ch.push(ruleTable(cfg.rules));
  340. } else {
  341. ch.push(p('无特殊业务规则'));
  342. }
  343. ch.push(h2(`${pre}5 变化和改进点`));
  344. (cfg.improvements || []).forEach(i => ch.push(bullet(i)));
  345. ch.push(h2(`${pre}6 权限管理需求`));
  346. if (cfg.permissions && cfg.permissions.length) {
  347. ch.push(permTable(cfg.permissions));
  348. } else {
  349. ch.push(p('参照第 9 章 权限管理汇总'));
  350. }
  351. ch.push(h2(`${pre}7 系统接口集成`));
  352. if (cfg.interfaces && cfg.interfaces.length) {
  353. ch.push(interfaceTable(cfg.interfaces));
  354. } else {
  355. ch.push(p('无外部系统接口;页面数据通过 Admin.NET 内置 API 获取'));
  356. }
  357. ch.push(h2(`${pre}8 报表需求`));
  358. if (cfg.reports && cfg.reports.length) {
  359. ch.push(reportTable(cfg.reports));
  360. } else {
  361. ch.push(p('无独立报表需求,相关数据通过看板和列表页面查看'));
  362. }
  363. return ch;
  364. }
  365. // ═══════════════════════════════════════════════════
  366. // 章节数据配置
  367. // ═══════════════════════════════════════════════════
  368. // ── Chapter 2: 供应商交货管理 ──
  369. const _ch2cfg = {
  370. targets: [
  371. '管理采购订单下达后的供应商交货执行过程,通过交货计划发布驱动供应商交期回复和发货单生成。',
  372. '支持供应商进行交期回复、生成发货单,跟踪在途、在检、入库全链路状态。',
  373. ],
  374. flowNote: '流程路径:供应商交货计划发布 → 供应商交期回复 → 生成发货单 → 在途/在检/入库跟踪。',
  375. flowSteps: [
  376. { label: '供应商交货计划发布', role: '采购跟单员' },
  377. { label: '供应商交期回复', role: '采购跟单员' },
  378. { label: '生成发货单', role: '采购跟单员' },
  379. { label: '在途/在检/入库跟踪', role: '系统自动' },
  380. ],
  381. activities: [
  382. ['10', '计划发布', '采购跟单员', '从MDP同步的交货计划中勾选发布至供应商,要求所选属同一供应商', 'MDP交货计划', '已发布交货计划'],
  383. ['20', '交期回复', '采购跟单员', '支持"按计划日期回复"(批量)和"回复交期"(手动选择)两种方式', '已发布记录', '交期已确认'],
  384. ['30', '生成发货单', '采购跟单员', '基于勾选交货记录,在弹窗中嵌入发货单表单生成发货单', '交货记录', '发货单'],
  385. ['40', '状态跟踪', '采购跟单员', '跟踪在途、在检、入库全链路状态,支持多维度筛选/排序/列设置', '发货数据', '状态视图'],
  386. ],
  387. rules: [
  388. ['1', '计划发布规则', '勾选发布时所有选中记录必须属于同一供应商'],
  389. ['2', '交期回复方式', '"按计划日期回复"为批量快速回复;"回复交期"为手动单日期回复'],
  390. ['3', '发货单生成', '基于已回复交期的交货记录生成发货单,嵌入式弹窗表单完成'],
  391. ['4', '状态跟踪', '系统自动同步在途/在检/入库状态,支持20个字段列设置'],
  392. ],
  393. improvements: [
  394. '从原系统手工Excel跟踪升级为在线交货管理,支持多维度筛选和批量操作。',
  395. '新增勾选发布和两种交期回复方式,替代原系统逐单操作低效方式。',
  396. '新增嵌入式发货单生成,在单个页面内完成"发布→回复→发货单"全流程。',
  397. '新增列设置功能,支持20个字段的自定义显示配置。',
  398. ],
  399. permissions: [
  400. ['1', '采购跟单员', '增删改查/发布/交期回复/生成发货单/关闭/导出'],
  401. ['2', '采购经理/仓库管理员', '查看权限'],
  402. ],
  403. interfaces: [
  404. ['1', 'MDP入站-采购订单', '从ERP同步采购订单数据,经DWD层转换为srm_polist_ds', '定时(日)'],
  405. ['2', 'MDP出站-交货发布', '发布后交货数据同步至SRM系统供供应商查看回复', '发布时触发'],
  406. ['3', 'CSV导出', '前端调用导出API生成CSV文件', '手动触发'],
  407. ],
  408. reports: [
  409. ['1', '交货执行报表', '按供应商/物料统计交货计划vs实际差异', '按周/月'],
  410. ['2', '交货单状态统计', '按状态(已发布/已回复/已发货/已入库)汇总', '按日/周'],
  411. ],
  412. };
  413. const ch2 = () => buildChapter('2', '供应商交货管理', _ch2cfg);
  414. // ── Chapter 3: 供应商发货单 ──
  415. const _ch3cfg = {
  416. targets: [
  417. '管理从交货管理生成的供应商发货单,支持标签生成、打印、附件管理和供应商发货跟踪。',
  418. '覆盖从发货单生成到供应商发货、入库关联的全链路操作。',
  419. ],
  420. flowNote: '流程路径:交货管理生成发货单 → 生成标签 → 上传附件 → 打印标签 → 供应商发货 → 打印发货单 → 入库关联。',
  421. flowSteps: [
  422. { label: '交货管理生成发货单', role: '采购跟单员' },
  423. { label: '生成标签', role: '采购跟单员' },
  424. { label: '上传附件', role: '采购跟单员' },
  425. { label: '打印标签', role: '采购跟单员' },
  426. { label: '供应商发货', role: '供应商' },
  427. { label: '打印发货单', role: '采购跟单员' },
  428. { label: '入库关联', role: '系统自动' },
  429. ],
  430. activities: [
  431. ['10', '生成发货单', '采购跟单员', '从交货管理页面勾选交货记录后一键生成发货单', '交货记录', '发货单'],
  432. ['20', '生成标签', '采购跟单员', '基于发货单信息生成物流标签(含物料编码、数量、供应商等)', '发货单', '标签'],
  433. ['30', '上传附件', '采购跟单员', '上传发货单相关附件(签收单、运输单、质检报告等)', '附件文件', '已关联附件'],
  434. ['40', '打印标签', '采购跟单员', '打印物流标签,粘贴于货物外包装用于物流识别', '标签', '打印标签'],
  435. ['50', '供应商发货', '供应商', '供应商按发货单要求进行实物发货', '发货单/标签', '已发货货物'],
  436. ['60', '打印发货单', '采购跟单员', '打印正式发货单据,作为运输和收货凭证', '发货单', '纸质发货单'],
  437. ['70', '入库关联', '系统', '收货数据入库后自动关联发货单并更新状态', '收货数据', '入库关联'],
  438. ],
  439. rules: [
  440. ['1', '发货单来源', '发货单仅由交货管理的"生成发货单"操作产生,不允许手动独立创建'],
  441. ['2', '标签规则', '标签基于发货单信息自动生成,含物料编码、数量、供应商等关键信息'],
  442. ['3', '附件管理', '支持多格式附件上传,大小限制按系统配置'],
  443. ['4', '打印控制', '标签和发货单均支持打印功能,打印前需校验发货单状态有效'],
  444. ],
  445. improvements: [
  446. '从原系统纸质发货单管理升级为在线管理,新增标签生成和打印功能。',
  447. '新增标签生成和打印环节,替代原系统手工标签制作。',
  448. '新增供应商发货状态跟踪,实现从生成到入库的全链路可视化。',
  449. '新增与入库数据的自动关联,发货单状态随入库数据同步更新。',
  450. ],
  451. permissions: [
  452. ['1', '采购跟单员', '增删改查/生成标签/上传附件/打印标签/打印发货单'],
  453. ['2', '仓库管理员', '查看权限'],
  454. ],
  455. interfaces: [
  456. ['1', '发货单数据同步', 'SupplierShipment表数据与ERP/SRM双向同步', '定时(日)'],
  457. ['2', '入库数据关联', '收货入库后自动关联发货单并更新状态', '入库时触发'],
  458. ['3', '核心数据表', 'SupplierShipment(供应商发货单数据)', '实时'],
  459. ],
  460. reports: [
  461. ['1', '发货单明细报表', '按供应商/时间段统计发货单生成、打印和发货数量', '按周/月'],
  462. ],
  463. };
  464. const ch3 = () => buildChapter('3', '供应商发货单', _ch3cfg);
  465. // ── Chapter 4: 采购退货单 ──
  466. const _ch4cfg = {
  467. targets: [
  468. '处理来料检验不合格后的退货流程,管理采购退货单据的创建、出库和状态跟踪。',
  469. '关联原始采购订单和不合格检验记录,确保退货流程的可追溯性。',
  470. ],
  471. flowNote: '流程路径:IQC检验不合格判定 → 退货单创建 → 退货出库 → 状态跟踪(新建→已出库→已完成)。',
  472. flowSteps: [
  473. { label: 'IQC检验不合格判定', role: '质量管理员' },
  474. { label: '退货单创建', role: '质量管理员' },
  475. { label: '退货出库', role: '仓库管理员' },
  476. { label: '状态跟踪(新建→已出库→已完成)', role: '系统自动' },
  477. ],
  478. activities: [
  479. ['10', '不合格判定', '质量管理员', 'IQC来料检验判定不合格,触发退货流程', 'IQC检验结果', '不合格判定'],
  480. ['20', '退货单创建', '质量管理员', '基于不合格判定创建退货单,关联原始采购订单和检验记录', '不合格判定', '退货单'],
  481. ['30', '退货出库', '仓库管理员', '执行退货出库操作,更新库存数据', '退货单', '出库记录'],
  482. ['40', '状态跟踪', '系统', '跟踪退货单处理进度:新建→已出库→已完成', '退货单数据', '状态更新'],
  483. ],
  484. rules: [
  485. ['1', '退货单来源', '退货单必须关联原始采购订单和不合格IQC检验记录'],
  486. ['2', '状态流转', '新建→已出库→已完成顺序流转,不可跳转'],
  487. ['3', '出库校验', '退货出库前需校验库存数量充足;出库后自动扣减库存'],
  488. ['4', '不可删除', '已出库的退货单不可删除,确保退货记录的完整性'],
  489. ],
  490. improvements: [
  491. '从原系统纸质退货流程升级为在线管理,与IQC检验结果自动联动。',
  492. '新增退货单状态生命周期管理,替代原系统简单"退货/不退货"二元状态。',
  493. '新增与采购订单的关联追溯,退货原因可反馈至供应商绩效评估。',
  494. ],
  495. permissions: [
  496. ['1', '质量管理员', '增删改查/创建退货单'],
  497. ['2', '仓库管理员', '查看/退货出库操作'],
  498. ['3', '采购经理', '查看权限'],
  499. ],
  500. interfaces: [
  501. ['1', 'IQC数据同步', '从IQC系统同步来料检验结果和不合格判定数据', '定时(日)'],
  502. ['2', '库存同步', '退货出库后同步扣减ERP库存数据', '出库时触发'],
  503. ['3', '核心数据表', 'PurchaseReturnOrder(采购退货单主表)', '实时'],
  504. ],
  505. reports: [
  506. ['1', '退货统计报表', '按供应商/物料/退货原因统计退货数量和金额', '按周/月'],
  507. ['2', '退货处理时效报表', '统计退货从创建到完成的平均处理时长', '按周/月'],
  508. ],
  509. };
  510. const ch4 = () => buildChapter('4', '采购退货单', _ch4cfg);
  511. // ── Chapter 5: IQC退货查询 ──
  512. const _ch5cfg = {
  513. targets: [
  514. '提供只读的退货历史查询功能,面向质量管理人员查看IQC检验不合格后的退货处理记录。',
  515. '展示退货详情,包括检验结果、不合格原因、处理方式等完整信息。',
  516. ],
  517. flowNote: '流程路径:IQC检验记录生成 → 不合格判定/退货关联 → 退货查询(检验单/物料/日期)。',
  518. flowSteps: [
  519. { label: 'IQC检验记录生成', role: '系统(MDP)' },
  520. { label: '不合格判定/退货关联', role: '质量管理员' },
  521. { label: '退货查询(检验单/物料/日期)', role: '质量管理员' },
  522. ],
  523. activities: [
  524. ['10', '检验记录生成', '系统(MDP)', '从IQC系统同步检验记录,生成检验数据', 'IQC检验数据', '检验记录'],
  525. ['20', '不合格判定/退货关联', '质量管理员', '对不合格检验结果进行判定并关联退货记录', '检验记录', '不合格判定/退货关联'],
  526. ['30', '退货查询', '质量管理员', '按检验单号、物料编码、日期范围查询退货记录和详情', '筛选条件', '退货查询结果'],
  527. ],
  528. rules: [
  529. ['1', '只读访问', '该页面为只读查询视图,不支持增删改操作,确保质量记录完整性'],
  530. ['2', '数据来源', '退货记录来自IQC检验系统的不合格判定和退货关联数据'],
  531. ['3', '可追溯性', '每条退货记录可关联到原始IQC检验单和采购订单'],
  532. ],
  533. improvements: [
  534. '从原系统纸质翻查升级为在线只读查询,检索效率大幅提升。',
  535. '新增检验结果、不合格原因、处理方式的结构化展示。',
  536. '退货记录与采购订单和供应商绩效关联,支持供应商质量评级。',
  537. ],
  538. permissions: [
  539. ['1', '质量管理员', '查看全部退货记录'],
  540. ['2', '采购经理/仓库管理员', '查看权限'],
  541. ],
  542. interfaces: [
  543. ['1', 'IQC数据同步', '从IQC系统同步检验结果和退货判定数据', '定时(日)'],
  544. ['2', '核心数据表', 'IQCReturnQuery(IQC退货查询视图)', '实时'],
  545. ],
  546. reports: [
  547. ['1', 'IQC退货分析报告', '按供应商/物料/不合格原因统计退货趋势', '按周/月'],
  548. ],
  549. };
  550. const ch5 = () => buildChapter('5', 'IQC退货查询', _ch5cfg);
  551. // ── Chapter 6: 采购执行看板主页 ──
  552. const _ch6cfg = {
  553. targets: [
  554. '通过可视化看板展示S4采购执行四大L1核心指标,帮助管理层全面掌握采购执行健康度。',
  555. '采用"双面板"设计——左侧物料采购执行(单品项) + 右侧供应商交付绩效(供应商)。',
  556. ],
  557. flowNote: '流程路径:MDP数据同步 → KPI指标计算引擎 → 双面板看板展示 → 多维筛选 → 导出报告/运营指标建模。',
  558. flowSteps: [
  559. { label: 'MDP数据同步/KPI指标计算', role: '系统(MDP)' },
  560. { label: '双面板看板展示(物料+供应商)', role: '采购经理' },
  561. { label: '多维筛选(日期/产品/采购单号)', role: '采购经理' },
  562. { label: '达成等级标签(优秀/良好/关注)', role: '采购经理' },
  563. { label: '导出报告/运营指标建模', role: '采购经理' },
  564. ],
  565. activities: [
  566. ['10', '数据加载', '系统', '从DWD/KPI层加载四大L1指标:交货周期、满足率、人效、在途周转', 'KPI数据库', 'KPI展示数据'],
  567. ['20', '左面板展示', '采购经理', '左侧"物料采购执行"按单品项展示KPI明细(指标名/公式/达成等级)', 'KPI数据', '物料维度面板'],
  568. ['30', '右面板展示', '采购经理', '右侧"供应商交付绩效"按供应商维度展示交付表现', '供应商数据', '供应商维度面板'],
  569. ['40', '基础查询', '采购经理', '按日期、产品、订单号、产线、供应商、采购单号、物料筛选', '筛选条件', '筛选后数据'],
  570. ['50', '操作功能', '采购经理', '导出报告、运营指标建模链接跳转', '看板数据', '报告文件/建模页'],
  571. ],
  572. rules: [
  573. ['1', '达成等级判定', '每个指标配有等级标签(优秀/良好/关注/预警),直观反映绩效水平'],
  574. ['2', '数据刷新', '通过MDP定时刷新或手动触发,确保看板数据时效性'],
  575. ['3', '指标口径对齐', '采购在途周转(S4-L1-D)与S3库存周转指标口径对齐'],
  576. ['4', '双面板联动', '左面板筛选条件变更时右面板数据同步更新'],
  577. ],
  578. improvements: [
  579. '从原系统零散Excel报表升级为可视化看板,左右双面板提供物料和供应商双视角分析。',
  580. '新增达成等级标签(优秀/良好/关注/预警),替代原系统单一数值展示。',
  581. '新增运营指标建模配置入口,支持自定义指标口径和计算规则。',
  582. ],
  583. permissions: [
  584. ['1', '采购经理', '查看/导出/指标建模'],
  585. ['2', '采购跟单员/质量管理员/仓管员', '查看权限'],
  586. ],
  587. interfaces: [
  588. ['1', '看板聚合API', 'GET /api/AidopKanban/module-detail?moduleCode=S4', '实时(页面加载)'],
  589. ['2', 'KPI数据源', 'S4-L1-A~D四大指标从DWD层计算产出', 'MDP定时刷新'],
  590. ['3', '运营指标建模', 'KPI口径通过运营指标建模模块配置', '配置驱动'],
  591. ],
  592. reports: [
  593. ['1', '采购执行月度报告', '含四大L1指标月度趋势和同比分析', '按月度'],
  594. ['2', '采购执行分析报告', '导出看板当前筛选条件下的分析结果', '按需'],
  595. ],
  596. };
  597. const ch6 = () => buildChapter('6', '采购执行看板主页', _ch6cfg);
  598. // ── Chapter 7: 供应商欠料看板 ──
  599. const _ch7cfg = {
  600. targets: [
  601. '提供未来15天(含当日D0)的供应商欠料滚动预测视图,帮助采购团队提前识别和应对缺料风险。',
  602. '基于WorkOrdDetailTotalKB宽表通过MRP计算工单物料齐套情况,预测未来周期的供应缺口。',
  603. ],
  604. flowNote: '流程路径:根据工单需求/BOM/库存/在途 → 15天滚动欠料计算 → 动态列展示 → 手动刷新。',
  605. flowSteps: [
  606. { label: '根据工单需求/BOM/库存/在途', role: '系统(MDP)' },
  607. { label: '15天滚动欠料计算', role: '系统' },
  608. { label: '动态列展示', role: '采购跟单员' },
  609. { label: '手动刷新', role: '采购跟单员' },
  610. ],
  611. activities: [
  612. ['10', '数据加载', '系统', '从WorkOrdDetailTotalKB宽表加载工单需求/BOM/库存/在途数据', 'MDP宽表', '齐套数据'],
  613. ['20', '欠料计算', '系统', '基于当前日期D0~D14共15天滚动欠料预测计算', '齐套数据', '欠料预测'],
  614. ['30', '动态列展示', '系统/采购跟单员', '15天日期列自动计算生成,右侧展示每日欠料数量', '欠料预测', '动态列视图'],
  615. ['40', '手动刷新', '采购跟单员', '点击刷新按钮执行实时缺料计算,页面不自动刷新', '刷新指令', '最新欠料数据'],
  616. ],
  617. rules: [
  618. ['1', '时间窗口规则', '自动计算D0(今日)~D14共15天滚动欠料预测,日期列动态生成'],
  619. ['2', '数据来源', '基于WorkOrdDetailTotalKB宽表,通过MRP计算工单物料齐套情况'],
  620. ['3', '刷新触发', '支持手动刷新执行实时欠料计算,页面不自动刷新'],
  621. ['4', '列设置', '支持自定义显示/隐藏15天日期列及基础信息列'],
  622. ],
  623. improvements: [
  624. '从原系统无欠料预测能力升级为15天滚动预测视图,帮助采购提前应对。',
  625. '新增动态日期列自动生成,日期变化时列自动滚动更新。',
  626. '新增按供应商维度的欠料筛选,快速定位责任供应商。',
  627. ],
  628. permissions: [
  629. ['1', '采购跟单员', '查看/刷新/筛选/排序'],
  630. ['2', '采购经理', '查看/导出权限'],
  631. ['3', '物料计划员/供应链经理', '查看权限'],
  632. ],
  633. interfaces: [
  634. ['1', 'MDP入站-齐套数据', '从ERP/MES/SRM同步工单需求、BOM、库存、在途数据', '定时(日)'],
  635. ['2', '欠料刷新API', '手动触发MRP欠料重新计算', '手动触发'],
  636. ['3', '核心数据表', 'WorkOrdDetailTotalKB(工单物料欠料宽表)', '实时'],
  637. ],
  638. reports: [
  639. ['1', '欠料预测日报', '每日输出D0~D14的欠料预测汇总', '日报'],
  640. ['2', '供应商欠料趋势分析', '按供应商统计历史欠料趋势', '按周/月'],
  641. ],
  642. };
  643. const ch7 = () => buildChapter('7', '供应商欠料看板', _ch7cfg);
  644. // ═══════════════════════════════════════════════════
  645. // Chapter 8: 数据中台与MDP集成
  646. // ═══════════════════════════════════════════════════
  647. function buildCh8() {
  648. const ch = [];
  649. ch.push(h1('8 数据中台与MDP集成'));
  650. ch.push(h2('8.1 目标/宗旨'));
  651. ch.push(p('S4采购执行模块的数据基础依赖于MDP数据中台,负责从ERP、SRM和IQC等外部系统抽取、清洗和转换采购订单、交货计划、收货数据和检验记录,为供应商交货管理、退货管理和采购执行看板提供统一的数据视图。'));
  652. ch.push(h2('8.2 分层设计'));
  653. ch.push(table(
  654. ['分层', '表前缀', '职责', '更新机制'],
  655. [
  656. ['贴源层(STG)', 'mdp_stg_*', '保留源系统原始数据', 'MDP同步覆盖写入'],
  657. ['标准层(STD)', 'mdp_std_*', '统一字段口径,清洗脏数据', 'MDP转换(最新批次)'],
  658. ['宽表层(DWD)', 'dwd_*', '面向页面和KPI主题事实表', 'MDP转换(最新批次)'],
  659. ['指标层(KPI)', 'ado_s9_kpi_value_*', '按天存储L1-L4指标值', 'MDP KPI计算'],
  660. ],
  661. [1500, 2000, 4200, 2300]
  662. ));
  663. ch.push(h2('8.3 S4核心数据流'));
  664. ch.push(bp('交货管理链路'));
  665. ch.push(p('ERP采购订单(purchase_order) → mdp_stg_po(贴源)→ mdp_std_po(标准)→ srm_polist_ds / SupplierShipment(DWD宽表)→ 交货页面'));
  666. ch.push(bp('退货管理链路'));
  667. ch.push(p('IQC检验结果(inspection_result) → mdp_stg_iqc(贴源)→ mdp_std_iqc(标准)→ PurchaseReturnOrder / IQCReturnQuery(DWD宽表)→ 退货页面'));
  668. ch.push(bp('看板/KPI链路'));
  669. ch.push(p('srm_polist_ds / ReceivingData / WorkOrdDetailTotalKB → DWD关联计算 → ado_s9_kpi_value_*(指标层)→ 看板消费'));
  670. ch.push(h2('8.4 数据表清单'));
  671. ch.push(table(
  672. ['序号', '表名/视图', '数据层级', '说明'],
  673. [
  674. ['1', 'srm_polist_ds', 'DWD', '采购订单交付计划(供应商交货主表)'],
  675. ['2', 'WorkOrdDetailTotalKB', 'DWD', '工单物料欠料宽表(含15天滚动预测)'],
  676. ['3', 'SupplierShipment', 'DWD', '供应商发货单数据'],
  677. ['4', 'PurchaseReturnOrder', 'DWD', '采购退货单主表'],
  678. ['5', 'IQCReturnQuery', 'DWD', 'IQC退货查询视图'],
  679. ['6', 'ReceivingData', 'DWD', '采购收货入库数据'],
  680. ],
  681. [600, 2200, 1200, 6000]
  682. ));
  683. ch.push(h2('8.5 MDP作业调度'));
  684. ch.push(table(
  685. ['配置项', '内容'],
  686. [
  687. ['作业编码', 'S4_MDP_SYNC_TRANSFORM'],
  688. ['批次格式', 'S4_MDP_FULL_yyyyMMddHHmmss'],
  689. ['数据源', 'ERP(purchase_order/receiving) / SRM(delivery_schedule/shipment) / IQC(inspection_result)'],
  690. ['同步方式', '入站同步(ODS) → DWD转换 → 业务视图/KPI输出'],
  691. ['监控入口', 'mdp_transform_run_log / mdp_sync_log / MDP运行监控页'],
  692. ['执行方式', 'Cron定时调度 + 手动触发'],
  693. ],
  694. [2200, 7800]
  695. ));
  696. ch.push(h2('8.6 变化和改进点'));
  697. ch.push(bullet('建立统一数据中台分层架构,S4业务数据从多源异构系统收敛到标准数据链路。'));
  698. ch.push(bullet('页面和看板统一从DWD/KPI层消费,确保数据口径一致。'));
  699. ch.push(bullet('MDP作业可观测,支持日志查询和异常追溯。'));
  700. return ch;
  701. }
  702. // ═══════════════════════════════════════════════════
  703. // Chapter 9: 权限管理汇总
  704. // ═══════════════════════════════════════════════════
  705. function buildCh9() {
  706. const ch = [];
  707. ch.push(h1('9 权限管理汇总'));
  708. ch.push(p('以下汇总S4采购执行模块各功能所涉及的角色和权限:'));
  709. ch.push(table(
  710. ['序号', '角色', '权限范围', '涉及模块'],
  711. [
  712. ['1', '采购跟单员', '增删改查交货管理、发货单;发布/交期回复/生成发货单/关闭/导出;查看欠料看板', '供应商交货管理、供应商发货单、供应商欠料看板'],
  713. ['2', '采购经理', '查看所有模块;查看采购执行看板和管理KPI;导出报告;指标建模', '全模块(查看+看板操作)'],
  714. ['3', '质量管理员', '增删改查采购退货单;查看IQC退货查询全部记录', '采购退货单、IQC退货查询'],
  715. ['4', '仓库管理员', '查看交货管理、发货单、退货单;执行退货出库操作', '交货管理、发货单、采购退货单'],
  716. ['5', '物料计划员', '查看供应商欠料看板(了解缺料预测)', '供应商欠料看板'],
  717. ['6', '供应链经理', '查看采购执行看板和供应商欠料看板', '采购执行看板、欠料看板'],
  718. ['7', 'IT运维人员', '查看MDP运行监控;监控同步任务状态', 'MDP运行监控'],
  719. ['8', '工厂厂长/总经理', '查看采购执行看板全部指标', '采购执行看板'],
  720. ],
  721. [600, 1600, 5000, 2800]
  722. ));
  723. return ch;
  724. }
  725. // ═══════════════════════════════════════════════════
  726. // 文档构建主流程
  727. // ═══════════════════════════════════════════════════
  728. async function buildDocument() {
  729. console.log('Generating flow chart images...');
  730. flowImageCache['overview'] = await generateOverviewFlowPNG();
  731. const allChapterConfigs = {
  732. '2': _ch2cfg, '3': _ch3cfg, '4': _ch4cfg, '5': _ch5cfg,
  733. '6': _ch6cfg, '7': _ch7cfg,
  734. };
  735. for (const [num, cfg] of Object.entries(allChapterConfigs)) {
  736. if (cfg.flowSteps) {
  737. flowImageCache[num] = await generateFlowPNG(cfg.flowSteps);
  738. }
  739. }
  740. console.log('Flow chart images generated.');
  741. const sections = [];
  742. // ── 封面 ──
  743. sections.push(empty(), empty(), empty());
  744. sections.push(new Paragraph({
  745. children: [new TextRun({ text: 'S4 采购执行模块', bold: true, size: 52, font: FONT, color: '1F4E79' })],
  746. alignment: AlignmentType.CENTER, spacing: { after: 100 },
  747. }));
  748. sections.push(new Paragraph({
  749. children: [new TextRun({ text: '蓝图设计方案', bold: true, size: 52, font: FONT, color: '1F4E79' })],
  750. alignment: AlignmentType.CENTER, spacing: { after: 600 },
  751. }));
  752. sections.push(new Paragraph({
  753. children: [new TextRun({ text: 'Ai-DOP 智慧运营决策平台', size: 28, font: FONT, color: '4472C4' })],
  754. alignment: AlignmentType.CENTER, spacing: { after: 120 },
  755. }));
  756. sections.push(new Paragraph({
  757. children: [new TextRun({ text: '版本:V1.0 日期:2026年5月27日 作者:AI Agent', size: 22, font: FONT, color: '808080' })],
  758. alignment: AlignmentType.CENTER, spacing: { after: 2400 },
  759. }));
  760. sections.push(new Paragraph({
  761. children: [new TextRun({ text: '【机密文档 · 仅限项目内部使用】', size: 21, font: FONT, color: 'C00000', italics: true })],
  762. alignment: AlignmentType.CENTER,
  763. }));
  764. // ── 文档控制 ──
  765. sections.push(pageBreak(), h1('文档控制'));
  766. sections.push(h2('更改记录'));
  767. sections.push(table(['日期', '姓名', '版本', '变更说明'],
  768. [['2026/05/27', 'AI Agent', 'V1.0', '初版(基于S4模块系统实现)']],
  769. [1800, 1200, 1000, 6000]));
  770. sections.push(empty());
  771. sections.push(h2('审核'));
  772. sections.push(table(['姓名', '职位', '签字/日期'], [['', '', ''], ['', '', '']], [3000, 3000, 4000]));
  773. sections.push(empty());
  774. sections.push(h2('发布'));
  775. sections.push(table(['编号', '名称', '地点'], [['', '', '']], [3000, 4000, 3000]));
  776. // ── 目录 ──
  777. sections.push(pageBreak(), h1('目录'));
  778. sections.push(...buildTOC());
  779. sections.push(pageBreak());
  780. // ── 第1章: 总体业务方案 ──
  781. sections.push(h1('1 总体业务方案'));
  782. sections.push(h2('1.1 目标和宗旨'));
  783. sections.push(bullet('S4采购执行模块是Ai-DOP智慧运营平台的核心执行模块之一,聚焦于采购订单下达后的交货执行、退货处理和采购执行可视化。'));
  784. sections.push(bullet('承接S3供应协同模块的采购订单输出,通过供应商交货管理、退货管理和采购执行看板三大核心能力,实现采购执行全流程的跟踪与管控。'));
  785. sections.push(bullet('核心KPI:物料交货周期(S4-L1-A)、物料交货满足率(S4-L1-B)、物料采购人效(S4-L1-C)、采购在途周转(S4-L1-D)。'));
  786. sections.push(bullet('注意:S4原本包含"采购管理"子模块(采购申请、订单、合同管理),该功能域已转移至S3供应协同模块管理。S4侧栏不再展示此入口。'));
  787. sections.push(h2('1.2 总体业务流程图'));
  788. sections.push(p('S4采购执行模块覆盖从"供应商交货"到"采购执行监控"的完整链路,下图展示了交货执行域、退货闭环域和采购执行看板域三大功能域的整体关系:'));
  789. sections.push(flowImageParagraph(flowImageCache['overview']));
  790. sections.push(h2('1.3 方案设计'));
  791. sections.push(p('S4模块采用"数据中台驱动 + 业务执行闭环"的架构模式,覆盖以下核心功能模块:'));
  792. sections.push(bullet('供应商交货管理:交货计划发布、交期回复、发货单生成、在途/在检/入库跟踪'));
  793. sections.push(bullet('供应商发货单:发货单查询、编辑、附件管理'));
  794. sections.push(bullet('采购退货单:退货单创建/审核/出库/状态跟踪,关联IQC检验'));
  795. sections.push(bullet('IQC退货查询:只读退货历史查询,质量追溯'));
  796. sections.push(bullet('采购执行看板主页:左右双面板KPI可视化(物料维度+供应商维度)'));
  797. sections.push(bullet('供应商欠料看板:15天滚动欠料预测,动态日期列展示'));
  798. sections.push(p('前端采用Vue 3 + Element Plus + TypeScript技术栈,交货管理使用AidopDemoShell容器组件+嵌入式发货单弹窗的组合布局,看板页使用KPI指标卡片+双面板对比布局。后端基于Admin.NET框架分层架构实现。数据通过MDP管道进行跨系统同步。'));
  799. // ── 第2~7章: 各功能模块 ──
  800. sections.push(pageBreak()); sections.push(h1('2 供应商交货管理')); sections.push(...ch2());
  801. sections.push(pageBreak()); sections.push(h1('3 供应商发货单')); sections.push(...ch3());
  802. sections.push(pageBreak()); sections.push(h1('4 采购退货单')); sections.push(...ch4());
  803. sections.push(pageBreak()); sections.push(h1('5 IQC退货查询')); sections.push(...ch5());
  804. sections.push(pageBreak()); sections.push(h1('6 采购执行看板主页')); sections.push(...ch6());
  805. sections.push(pageBreak()); sections.push(h1('7 供应商欠料看板')); sections.push(...ch7());
  806. // ── 第8~9章 ──
  807. sections.push(pageBreak()); sections.push(...buildCh8());
  808. sections.push(pageBreak()); sections.push(...buildCh9());
  809. return sections;
  810. }
  811. // ═══════════════════════════════════════════════════
  812. // 生成文档
  813. // ═══════════════════════════════════════════════════
  814. async function main() {
  815. const doc = new Document({
  816. styles: { default: { document: { run: { font: FONT, size: 21 } } } },
  817. sections: [{
  818. properties: { page: { margin: { top: convertInchesToTwip(0.8), bottom: convertInchesToTwip(0.8), left: convertInchesToTwip(1.0), right: convertInchesToTwip(1.0) } } },
  819. children: await buildDocument(),
  820. }],
  821. });
  822. const buffer = await Packer.toBuffer(doc);
  823. const outPath = 'd:\\DEMONET\\doc\\S4采购执行模块蓝图设计方案.docx';
  824. const fallbackPath = 'd:\\DEMONET\\doc\\S4采购执行模块蓝图设计方案_V2.docx';
  825. const tempPath = 'd:\\DEMONET\\doc\\S4_blueprint_temp.docx';
  826. fs.writeFileSync(tempPath, buffer);
  827. try { fs.unlinkSync(outPath); } catch(e) {}
  828. try { fs.renameSync(tempPath, outPath); console.log(`Renamed to: ${outPath}`); }
  829. catch(e) {
  830. try { fs.unlinkSync(fallbackPath); } catch(_) {}
  831. try { fs.renameSync(tempPath, fallbackPath); console.log(`Fallback to: ${fallbackPath}`); }
  832. catch(e2) { console.log(`Generated: ${tempPath} (target locked)`); }
  833. }
  834. console.log(`Size: ${(buffer.length / 1024).toFixed(1)} KB`);
  835. }
  836. main().catch(err => { console.error('Error:', err); process.exit(1); });