const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, Header, Footer, AlignmentType, HeadingLevel, BorderStyle, WidthType, ShadingType, VerticalAlign, PageNumber } = require("C:/Users/ADMINI~1/AppData/Local/Temp/docx-gen/node_modules/docx"); const fs = require("fs"); const NAVY = "0A2A70"; const HEADER_BG = "123A7A"; const ALT = "F4F7FB"; const LINE = "C5D0E0"; const MUTED = "5B6775"; const thin = { style: BorderStyle.SINGLE, size: 4, color: LINE }; const borders = { top: thin, bottom: thin, left: thin, right: thin, insideHorizontal: thin, insideVertical: thin }; const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; const noBorders = { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder }; const font = { ascii: "Times New Roman", eastAsia: "SimSun", hAnsi: "Times New Roman" }; const fontHei = { ascii: "Times New Roman", eastAsia: "SimHei", hAnsi: "Times New Roman" }; function run(text, opts = {}) { return new TextRun({ text, font: opts.hei ? fontHei : font, size: opts.size || 21, bold: !!opts.bold, color: opts.color || "000000", italics: !!opts.italics, }); } function p(text, opts = {}) { return new Paragraph({ spacing: { before: opts.before ?? 60, after: opts.after ?? 60, line: 312 }, alignment: opts.align || AlignmentType.JUSTIFIED, indent: opts.indent ? { firstLine: 480 } : undefined, keepNext: !!opts.keepNext, children: [run(text, opts)], }); } function h1(text) { return new Paragraph({ heading: HeadingLevel.HEADING_1, spacing: { before: 280, after: 140, line: 312 }, border: { bottom: { style: BorderStyle.SINGLE, size: 12, color: NAVY, space: 4 } }, children: [run(text, { hei: true, size: 32, bold: true, color: NAVY })], }); } function h2(text) { return new Paragraph({ heading: HeadingLevel.HEADING_2, spacing: { before: 240, after: 100, line: 312 }, children: [run(text, { hei: true, size: 28, bold: true, color: NAVY })], }); } function cell(text, opts = {}) { const fill = opts.fill; return new TableCell({ borders, width: { size: opts.w, type: WidthType.DXA }, shading: fill ? { type: ShadingType.CLEAR, fill } : undefined, margins: { top: 50, bottom: 50, left: 70, right: 70 }, verticalAlign: VerticalAlign.CENTER, children: [ new Paragraph({ spacing: { before: 20, after: 20, line: 276 }, alignment: opts.center ? AlignmentType.CENTER : AlignmentType.LEFT, children: [run(String(text), { size: opts.size || 18, bold: !!opts.bold, color: opts.color || "000000" })], }), ], }); } function table(headers, rows, widths) { const total = widths.reduce((a, b) => a + b, 0); const headerRow = new TableRow({ tableHeader: true, cantSplit: true, children: headers.map((h, i) => cell(h, { w: widths[i], fill: HEADER_BG, bold: true, color: "FFFFFF", size: 18, center: true })), }); const body = rows.map((row, ri) => new TableRow({ cantSplit: true, children: row.map((val, i) => cell(val, { w: widths[i], fill: ri % 2 ? ALT : "FFFFFF", size: 18, center: i === 0 })), })); return new Table({ width: { size: total, type: WidthType.DXA }, columnWidths: widths, borders, rows: [headerRow, ...body], }); } const W = [700, 1400, 1900, 3400, 1800, 900]; // 10100 usable-ish; page content width ~ 11906-1701-1417 = 8788 // Recalc content width: 11906 - 1418 - 1134 = 9354. Use slightly narrower margins. const content = 9354; const patrolW = [620, 1200, 1700, 2800, 1934, 1100]; const sysW = [620, 1400, 1400, 2600, 2234, 1100]; const stopW = [700, 2800, 5854]; const patrol = [ ["1", "巡检任务", "task-summary", "cards/taskSummary/inspectionTask", "patrol:task:list", "dashboard"], ["2", "实时监控", "video-preview", "video/LivePreview", "video:preview:list", "monitor"], ["3", "巡检方案", "patrol-plan", "cards/patrolPlan/notstart", "patrol:plan:list", "task"], ["4", "点位管理", "site-management", "cards/site_management/site_management", "patrol:point:list", "tree"], ["5", "巡检报告", "patrol-report", "cards/patrolReport/InspectionReporter", "patrol:report:list", "documentation"], ["6", "巡检计划", "patrol-schedule", "cards/tasksShow/index", "patrol:schedule:list", "date"], ["7", "缺陷记录", "defect-record", "cards/defectRecord/index", "patrol:defect:list", "bug"], ["8", "历史回放", "video-playback", "video/HistoryPlayback", "video:playback:list", "time"], ]; const system = [ ["1", "用户管理", "user", "system/user/index", "system:user:list", "user"], ["2", "角色管理", "role", "system/role/index", "system:role:list", "peoples"], ["3", "菜单管理", "menu", "system/menu/index", "system:menu:list", "tree-table"], ["4", "部门管理", "dept", "system/dept/index", "system:dept:list", "tree"], ["5", "岗位管理", "post", "system/post/index", "system:post:list", "post"], ["6", "字典管理", "dict", "system/dict/index", "system:dict:list", "dict"], ["7", "参数设置", "config", "system/config/index", "system:config:list", "edit"], ["8", "通知公告", "notice", "system/notice/index", "system:notice:list", "message"], ]; const stop = [ ["1", "问题清单", "不是当前左侧菜单,停用"], ["2", "巡视任务管理 / 巡检任务管理", "旧名称,与「巡检任务」重复,停用"], ["3", "联动动作配置", "当前系统未使用,停用"], ["4", "设备管理", "当前左侧不展示,停用"], ["5", "点位配置", "另一套点位页面,与「点位管理」重复,停用"], ["6", "区域管理", "当前左侧不展示,停用"], ["7", "动环巡检(现有停用的旧目录)", "不要改旧记录,按本文新建一条目录"], ["8", "系统监控", "若依自带,当前系统不用,停用"], ["9", "系统工具", "若依自带,当前系统不用,停用"], ["10", "测试官网", "若依自带,停用"], ["11", "基础数据", "当前左侧不展示,停用"], ["12", "巡视管理", "旧目录,停用"], ]; const hiddenPages = [ ["1", "巡检报告详情", "从巡检报告点进去,不要单独建菜单"], ["2", "分配角色", "从用户管理点进去,不要单独建菜单"], ["3", "分配用户", "从角色管理点进去,不要单独建菜单"], ["4", "字典数据", "从字典管理点进去,不要单独建菜单"], ]; const children = []; children.push(new Paragraph({ spacing: { before: 0, after: 40, line: 312 }, children: [run("动环智能巡检系统", { hei: true, size: 22, color: MUTED })], })); children.push(new Paragraph({ spacing: { before: 40, after: 80, line: 360 }, border: { bottom: { style: BorderStyle.SINGLE, size: 16, color: NAVY, space: 6 } }, children: [run("菜单整理说明(供测试录入)", { hei: true, size: 36, bold: true, color: NAVY })], })); children.push(p("录入位置:系统管理 → 菜单管理。按本文新建两组菜单,并把旧菜单停用。普通账号登录后,左侧只应看到「动环巡检」下的 8 项,顺序与下表一致。", { indent: true, size: 24 })); children.push(p("管理员从顶部进入「系统管理」后,左侧应直接展开用户管理、角色管理等 8 项,不再套一层「系统管理」。", { indent: true, size: 24, before: 40 })); children.push(h1("一、填写规则")); children.push(p("菜单类型按若依约定:目录填 M,菜单填 C,按钮填 F。本文只建目录和菜单,不建按钮。", { indent: true, size: 24 })); children.push(p("路由地址不要带前导斜杠。组件路径相对 src/views,不要写 .vue,也不要写开头的斜杠。显示状态一律选「显示」,菜单状态一律选「正常」。是否外链选「否」,是否缓存可按页面默认。", { indent: true, size: 24 })); children.push(p("父级只建目录,组件路径留空。子菜单的上级菜单选对应目录。排序按表中数字填写,数字越小越靠前。", { indent: true, size: 24 })); children.push(p("权限标识先按表填写。若后端角色里还没有这些标识,给管理员角色勾上对应菜单即可;普通角色只勾「动环巡检」及其 8 个子菜单。", { indent: true, size: 24 })); children.push(h1("二、动环巡检")); children.push(p("先新增一条目录,再把下面 8 条菜单挂到它下面。", { indent: true, size: 24, keepNext: true })); children.push(new Paragraph({ keepNext: true, spacing: { before: 80, after: 60, line: 312 }, children: [run("表 1 父级目录:动环巡检", { bold: true, size: 21 })], })); children.push(table( ["字段", "填写内容"], [ ["菜单名称", "动环巡检"], ["上级菜单", "主类目"], ["菜单类型", "目录(M)"], ["显示排序", "1"], ["路由地址", "inspection"], ["组件路径", "留空"], ["菜单图标", "dashboard"], ["显示状态", "显示"], ["菜单状态", "正常"], ], [2200, 7154] )); children.push(new Paragraph({ keepNext: true, spacing: { before: 200, after: 60, line: 312 }, children: [run("表 2 动环巡检下的菜单(上级菜单均选「动环巡检」,类型均为菜单 C)", { bold: true, size: 21 })], })); children.push(table( ["排序", "菜单名称", "路由地址", "组件路径", "权限标识", "图标"], patrol, patrolW )); children.push(h1("三、系统管理")); children.push(p("只分配给管理员角色。若库里已有名为「系统管理」、路由为 system 的目录,直接在它下面按本表核对或补齐,不要再新建第二条同名目录。子菜单已存在且路由、组件一致的,只改名称、排序和图标,不要重复新增。", { indent: true, size: 24, keepNext: true })); children.push(new Paragraph({ keepNext: true, spacing: { before: 80, after: 60, line: 312 }, children: [run("表 3 父级目录:系统管理", { bold: true, size: 21 })], })); children.push(table( ["字段", "填写内容"], [ ["菜单名称", "系统管理"], ["上级菜单", "主类目"], ["菜单类型", "目录(M)"], ["显示排序", "2"], ["路由地址", "system"], ["组件路径", "留空"], ["菜单图标", "system"], ["显示状态", "显示"], ["菜单状态", "正常"], ], [2200, 7154] )); children.push(new Paragraph({ keepNext: true, spacing: { before: 200, after: 60, line: 312 }, children: [run("表 4 系统管理下的菜单(上级菜单均选「系统管理」,类型均为菜单 C)", { bold: true, size: 21 })], })); children.push(table( ["排序", "菜单名称", "路由地址", "组件路径", "权限标识", "图标"], system, sysW )); children.push(h1("四、需要停用的旧菜单")); children.push(p("这些是若依或旧巡视系统残留,当前页面用不到。处理方式是把菜单状态改为「停用」,不要物理删除,避免角色、按钮权限丢关联。停用后重新登录,左侧不应再出现这些名称。", { indent: true, size: 24, keepNext: true })); children.push(new Paragraph({ keepNext: true, spacing: { before: 80, after: 60, line: 312 }, children: [run("表 5 停用清单", { bold: true, size: 21 })], })); children.push(table(["序号", "菜单名称", "处理说明"], stop, stopW)); children.push(h2("不要单独建菜单的页面")); children.push(p("下面这些页面从其他菜单点进去,前端已有隐藏路由。菜单管理里不要再加同名菜单。", { indent: true, size: 24, keepNext: true })); children.push(new Paragraph({ keepNext: true, spacing: { before: 80, after: 60, line: 312 }, children: [run("表 6 隐藏页面", { bold: true, size: 21 })], })); children.push(table(["序号", "页面", "说明"], hiddenPages, stopW)); children.push(h1("五、录入后核对")); children.push(p("用管理员账号退出后重新登录。左侧第一组应为:巡检任务、实时监控、巡检方案、点位管理、巡检报告、巡检计划、缺陷记录、历史回放。点每一项都能打开对应页面,地址分别是 /task-summary、/video-preview、/patrol-plan、/site-management、/patrol-report、/patrol-schedule、/defect-record、/video-playback。", { indent: true, size: 24 })); children.push(p("从顶部进入系统管理后,左侧应依次为:用户管理、角色管理、菜单管理、部门管理、岗位管理、字典管理、参数设置、通知公告。", { indent: true, size: 24 })); children.push(p("用普通账号登录,左侧只有动环巡检的 8 项,看不到系统管理,也看不到第四节里已停用的旧菜单。", { indent: true, size: 24 })); children.push(p("若某一项能看到但点开是 404,先核对组件路径是否与表中完全一致,包括大小写和下划线。路由地址多写了前导斜杠时,也会和现有页面地址对不上。", { indent: true, size: 24 })); const doc = new Document({ styles: { default: { document: { styles: [{ id: "Normal", run: { font: "SimSun", size: 24 } }] }, }, }, sections: [{ properties: { page: { size: { width: 11906, height: 16838 }, margin: { top: 1134, bottom: 1134, left: 1134, right: 1134 }, }, }, headers: { default: new Header({ children: [new Paragraph({ border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: NAVY, space: 6 } }, spacing: { after: 80, line: 276 }, children: [ run("动环智能巡检系统", { size: 18, color: NAVY, bold: true }), run(" 菜单整理说明", { size: 18, color: MUTED }), ], })], }), }, footers: { default: new Footer({ children: [new Paragraph({ alignment: AlignmentType.CENTER, border: { top: { style: BorderStyle.SINGLE, size: 6, color: LINE, space: 6 } }, spacing: { before: 60, line: 276 }, children: [ run("第 ", { size: 18, color: MUTED }), new TextRun({ children: [PageNumber.CURRENT], font, size: 18, color: MUTED }), run(" 页", { size: 18, color: MUTED }), ], })], }), }, children, }], }); Packer.toBuffer(doc).then((buf) => { const out = "E:/work/yanyuan/webs-admin/docs/菜单整理说明-测试录入.docx"; fs.mkdirSync("E:/work/yanyuan/webs-admin/docs", { recursive: true }); fs.writeFileSync(out, buf); console.log("wrote", out, buf.length); });