test(运单资料文件包下载): 需求文档与 QE Fleet 全流程测试设计产物入库
- 需求 v1.0 定稿(md+docx)与 UI 原型入库;确认单判定 docx 为同文载体、安徽运八并行无冲突 - 全流程产物:分析 + 59 测试点 + 85 用例 + 测试数据 145 组;评审 3 阻断与覆盖缺口 GAP-001 已修复,裁决 PASS_WITH_FIX - 导出 Excel(云效模型)+ XMind + 版本快照 v1;含 docx 生成脚本与转换校验产物 - .gitignore 排除 node_modules 与 Excel 锁文件
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
// 生成《运单资料文件包下载需求文档》Word 版
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
|
||||
WidthType, AlignmentType, HeadingLevel, ShadingType, BorderStyle,
|
||||
LevelFormat, VerticalAlign, Footer, PageNumber,
|
||||
} = require("docx");
|
||||
|
||||
const FONT_BODY = { ascii: "Times New Roman", hAnsi: "Times New Roman", eastAsia: "宋体" };
|
||||
const FONT_HEAD = { ascii: "Arial", hAnsi: "Arial", eastAsia: "黑体" };
|
||||
const FONT_MONO = { ascii: "Consolas", hAnsi: "Consolas", eastAsia: "宋体" };
|
||||
|
||||
const T = (t, o = {}) => new TextRun({ text: t, ...o });
|
||||
const B = (t, o = {}) => new TextRun({ text: t, bold: true, ...o });
|
||||
|
||||
function para(children, opts = {}) {
|
||||
return new Paragraph({ spacing: { after: 140, line: 312 }, children: Array.isArray(children) ? children : [T(children)], ...opts });
|
||||
}
|
||||
function bullet(children) {
|
||||
return new Paragraph({ numbering: { reference: "bul", level: 0 }, spacing: { after: 80, line: 300 }, children: Array.isArray(children) ? children : [T(children)] });
|
||||
}
|
||||
function numItem(children) {
|
||||
return new Paragraph({ numbering: { reference: "num", level: 0 }, spacing: { after: 80, line: 300 }, children: Array.isArray(children) ? children : [T(children)] });
|
||||
}
|
||||
const h1 = (t) => new Paragraph({ heading: HeadingLevel.HEADING_1, children: [T(t)] });
|
||||
const h2 = (t) => new Paragraph({ heading: HeadingLevel.HEADING_2, children: [T(t)] });
|
||||
const h3 = (t) => new Paragraph({ heading: HeadingLevel.HEADING_3, children: [T(t)] });
|
||||
const spacer = (after = 120) => new Paragraph({ spacing: { after }, children: [] });
|
||||
|
||||
function noteP(children) {
|
||||
return new Paragraph({
|
||||
spacing: { after: 160, line: 300 }, indent: { left: 200 },
|
||||
shading: { type: ShadingType.CLEAR, fill: "F0F7FF" },
|
||||
border: { left: { style: BorderStyle.SINGLE, size: 18, color: "1677FF", space: 6 } },
|
||||
children: Array.isArray(children) ? children : [T(children)],
|
||||
});
|
||||
}
|
||||
|
||||
function monoBlock(lines) {
|
||||
return lines.map((l) => new Paragraph({
|
||||
spacing: { after: 0, line: 264 }, indent: { left: 240, right: 240 },
|
||||
shading: { type: ShadingType.CLEAR, fill: "F7F7F7" },
|
||||
children: [new TextRun({ text: l, font: FONT_MONO, size: 17 })],
|
||||
})).concat([spacer(140)]);
|
||||
}
|
||||
|
||||
function makeTable(headers, rows, widths, { size = 18, centerCols = [] } = {}) {
|
||||
const cellPara = (txt, center, bold, fsize) => new Paragraph({
|
||||
alignment: center ? AlignmentType.CENTER : AlignmentType.LEFT,
|
||||
spacing: { after: 0, line: 264 },
|
||||
children: [new TextRun({ text: txt, bold, size: fsize })],
|
||||
});
|
||||
const headerRow = new TableRow({
|
||||
tableHeader: true,
|
||||
children: headers.map((h, i) => new TableCell({
|
||||
width: { size: widths[i], type: WidthType.DXA },
|
||||
shading: { type: ShadingType.CLEAR, fill: "DCE6F1" },
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
margins: { top: 70, bottom: 70, left: 110, right: 110 },
|
||||
children: [cellPara(h, true, true, size)],
|
||||
})),
|
||||
});
|
||||
const bodyRows = rows.map((r) => new TableRow({
|
||||
children: r.map((c, i) => new TableCell({
|
||||
width: { size: widths[i], type: WidthType.DXA },
|
||||
verticalAlign: VerticalAlign.CENTER,
|
||||
margins: { top: 60, bottom: 60, left: 110, right: 110 },
|
||||
children: (Array.isArray(c) ? c : [String(c)]).map((line) => cellPara(line, centerCols.includes(i), false, size)),
|
||||
})),
|
||||
}));
|
||||
return new Table({
|
||||
width: { size: widths.reduce((a, b) => a + b, 0), type: WidthType.DXA },
|
||||
columnWidths: widths,
|
||||
rows: [headerRow, ...bodyRows],
|
||||
});
|
||||
}
|
||||
|
||||
const kids = [];
|
||||
|
||||
// ── 标题区 ──
|
||||
kids.push(new Paragraph({
|
||||
alignment: AlignmentType.CENTER, spacing: { before: 200, after: 80 },
|
||||
children: [new TextRun({ text: "运单资料文件包下载 需求文档", bold: true, size: 36, font: FONT_HEAD, color: "1F3864" })],
|
||||
}));
|
||||
kids.push(new Paragraph({
|
||||
alignment: AlignmentType.CENTER, spacing: { after: 60 },
|
||||
children: [new TextRun({ text: "版本 v1.0(定稿) · 运八平台端 · 2026-09-08", size: 18, color: "888888" })],
|
||||
}));
|
||||
kids.push(new Paragraph({ spacing: { after: 240 }, border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: "1677FF", space: 4 } }, children: [] }));
|
||||
|
||||
// ── 文档信息 ──
|
||||
kids.push(makeTable(
|
||||
["项", "内容"],
|
||||
[
|
||||
["功能名称", "运单资料文件包下载(新增菜单)"],
|
||||
["所属端", "运八平台端(Web 管理后台)"],
|
||||
["版本", "v1.0(定稿)"],
|
||||
["编写日期", "2026-09-08"],
|
||||
["确认记录", "2026-09-08 需求方确认,13 项待确认事项全部采纳本文档建议方案(见第八章)"],
|
||||
["需求来源", "运营口头需求(2026-09-08),本文档为整理稿"],
|
||||
["关联术语", "以 knowledge_base/01_standards/terminology.md 为准"],
|
||||
["原型", "resources/prototypes/anhuibaba_file_package.html(静态演示:任务列表 / 上传弹窗 / 文件包结构预览 / 失败清单)"],
|
||||
],
|
||||
[1800, 7226]
|
||||
));
|
||||
kids.push(spacer());
|
||||
|
||||
// ── 一、需求背景与目标 ──
|
||||
kids.push(h1("一、需求背景与目标"));
|
||||
kids.push(para("运营人员在线下归档、审计、对账等场景中,需要按批次收集运单资料:运单合同、电子回单,以及司机证件照片、车辆证件照片。目前只能逐单进入运单详情手工下载整理,效率低且易遗漏。"));
|
||||
kids.push(para([B("目标:"), T("提供一个“上传运单清单 → 系统自动生成资料文件包 → 任务完成后下载”的批量导出能力,按“司机+身份证+车辆”自动归批,资料按运单分目录组织、按规则命名,一次下载即可获得全部资料。")]));
|
||||
kids.push(para([B("本期范围:"), T("仅做资料导出打包;不新增数据采集,合同、电子回单、司机证件、车辆证件均读取运八平台现有数据。")]));
|
||||
|
||||
// ── 二、名词定义 ──
|
||||
kids.push(h1("二、名词定义"));
|
||||
kids.push(makeTable(
|
||||
["术语", "定义"],
|
||||
[
|
||||
["批次", "上传清单中,运单在系统内的 司机姓名 + 司机身份证号 + 车牌号 三要素完全一致的一组运单。"],
|
||||
["文件包(批次包)", "一个批次生成的目录,内含该批次全部运单的运单包。"],
|
||||
["运单包", "批次包下按运单维度建立的子目录,内含该运单的合同、电子回单及(仅最小序号运单)司机、车辆证件照片。"],
|
||||
["最小序号运单", "同一批次内,上传清单中“序号”最小的运单。该运单的运单包额外包含司机证件照片、车辆证件照片。"],
|
||||
],
|
||||
[1900, 7126]
|
||||
));
|
||||
kids.push(spacer());
|
||||
|
||||
// ── 三、整体流程 ──
|
||||
kids.push(h1("三、整体流程"));
|
||||
kids.push(...monoBlock([
|
||||
"运营人员 系统",
|
||||
" │ ① 下载模板,填写运单清单并上传 │",
|
||||
" │ ────────────────────────────────────→ │ ② 校验模板与数据(见 4.2.2)",
|
||||
" │ │ ③ 创建打包任务(待处理 → 处理中)",
|
||||
" │ ④ 任务列表查看处理进度 │ ④ 按批次分组 → 逐运单下载关联文件 → 组装文件包",
|
||||
" │ ←──────────────────────────────────── │ ⑤ 任务完成(已完成 / 已完成·部分失败 / 失败)",
|
||||
" │ ⑥ 点击“下载”获取文件包 zip │",
|
||||
" │ ←──────────────────────────────────── │ ⑦ 返回 zip,记录下载审计日志",
|
||||
]));
|
||||
kids.push(para([T("核心规则:", { bold: true }), B("司机+身份证+车辆算一批运单,生成一个文件包"), T(";文件包内按运单建运单包;批次内"), B("最小序号"), T("的运单包额外含司机及车辆证件照片,其余运单包只含合同与电子回单。")]));
|
||||
|
||||
// ── 四、功能需求 ──
|
||||
kids.push(h1("四、功能需求"));
|
||||
|
||||
kids.push(h2("4.1 菜单入口"));
|
||||
kids.push(bullet([T("运八平台端新增独立菜单,菜单名称 "), B("“运单文件包”"), T(",挂在运单管理相关模块下。")]));
|
||||
kids.push(bullet("页面结构:上方“上传任务”入口(模板下载 + 上传按钮),下方“任务列表”。"));
|
||||
kids.push(bullet([T("可见角色:"), B("运营人员"), T(";菜单需权限控制——文件包含身份证等敏感证件照片。")]));
|
||||
|
||||
kids.push(h2("4.2 文件上传"));
|
||||
kids.push(h3("4.2.1 上传模板"));
|
||||
kids.push(makeTable(
|
||||
["列名", "类型", "必填", "说明"],
|
||||
[
|
||||
["序号", "整数", "是", "正整数,全文件内唯一;决定批次内“最小序号运单”的判定"],
|
||||
["货源单号", "文本", "是", "系统内货源单号"],
|
||||
["运单号", "文本", "是", "系统内运单号"],
|
||||
],
|
||||
[1300, 1100, 700, 5926],
|
||||
{ centerCols: [1, 2] }
|
||||
));
|
||||
kids.push(spacer(80));
|
||||
kids.push(bullet([T("文件格式:"), B(".xlsx"), T(";页面提供模板下载。")]));
|
||||
kids.push(bullet([T("限制:单文件行数上限 "), B("2000 行"), T(";文件大小上限 "), B("10MB"), T("。")]));
|
||||
|
||||
kids.push(h3("4.2.2 校验规则"));
|
||||
kids.push(makeTable(
|
||||
["级别", "规则", "处理方式"],
|
||||
[
|
||||
["文件级", "非 xlsx 格式 / 列名缺失或不符 / 文件损坏", "上传即拒绝,提示具体原因,不创建任务"],
|
||||
["文件级", "行数、大小超上限", "上传即拒绝"],
|
||||
["数据级", "序号非正整数或重复", "该行计入失败清单"],
|
||||
["数据级", "货源单号 / 运单号为空", "该行计入失败清单"],
|
||||
["数据级", "运单号在系统中不存在", "该行计入失败清单"],
|
||||
["数据级", "文件中货源单号与该运单实际所属货源不一致", "该行计入失败清单"],
|
||||
["数据级", "同一运单号在文件中重复出现", "重复行计入失败清单"],
|
||||
["数据级", "运单缺少司机或车辆信息,无法归批", "该行计入失败清单(原因:无法归批)"],
|
||||
],
|
||||
[900, 4400, 3726],
|
||||
{ centerCols: [0] }
|
||||
));
|
||||
kids.push(spacer(80));
|
||||
kids.push(noteP([B("处理原则:"), T("文件级错误整体拒绝;数据级错误剔除失败行后继续处理有效行,任务状态记为“已完成·部分失败”。")]));
|
||||
|
||||
kids.push(h2("4.3 任务"));
|
||||
kids.push(h3("4.3.1 任务状态"));
|
||||
kids.push(...monoBlock([
|
||||
"待处理 → 处理中 → 已完成 (全部有效行成功)",
|
||||
" → 已完成·部分失败 (部分行失败,可下载失败清单)",
|
||||
" → 失败 (系统级错误导致整体未生成文件包)",
|
||||
]));
|
||||
kids.push(h3("4.3.2 任务列表"));
|
||||
kids.push(makeTable(
|
||||
["字段", "说明"],
|
||||
[
|
||||
["任务编号", "系统生成"],
|
||||
["源文件名", "上传的清单文件名"],
|
||||
["上传人 / 上传时间", "—"],
|
||||
["任务状态", "待处理 / 处理中 / 已完成 / 已完成·部分失败 / 失败"],
|
||||
["总行数 / 成功数 / 失败数", "处理进度与结果统计"],
|
||||
["完成时间", "—"],
|
||||
["操作", "下载文件包(任务成功后显示)、下载失败清单(存在失败行时显示,xlsx,含序号/货源单号/运单号/失败原因)"],
|
||||
],
|
||||
[2500, 6526]
|
||||
));
|
||||
kids.push(spacer());
|
||||
|
||||
kids.push(h2("4.4 文件包生成规则"));
|
||||
kids.push(h3("4.4.1 批次划分"));
|
||||
kids.push(para([T("按有效行运单在"), B("系统内"), T("的 司机姓名 + 司机身份证号 + 车牌号 三要素归组,三要素完全一致归为一批,每批生成一个文件包。同一司机换车、同一车辆换司机都会拆分为不同批次。")]));
|
||||
kids.push(h3("4.4.2 目录结构与命名"));
|
||||
kids.push(...monoBlock([
|
||||
"运单资料_【任务编号】.zip ← 任务下载总包",
|
||||
"├─ 【司机姓名】-【身份证号】-【车牌号】/ ← 批次文件包①",
|
||||
"│ ├─ 1-YD20260901001-HY202609001-含司机及车辆证件/ ← 批次内最小序号运单包",
|
||||
"│ │ ├─ 合同/(该运单全部合同文件)",
|
||||
"│ │ ├─ 电子回单/(该运单全部支付记录对应回单)",
|
||||
"│ │ ├─ 司机证件/(身份证、驾驶证、从业资格证等照片)",
|
||||
"│ │ └─ 车辆证件/(行驶证、道路运输证等照片)",
|
||||
"│ └─ 3-YD20260901003-HY202609001/ ← 批次内其余运单包",
|
||||
"│ ├─ 合同/",
|
||||
"│ └─ 电子回单/",
|
||||
"└─ 【司机姓名】-【身份证号】-【车牌号】/ ← 批次文件包②",
|
||||
" └─ 2-YD20260901002-HY202609002-含司机及车辆证件/ ← 单运单批次,仍含证件",
|
||||
]));
|
||||
kids.push(bullet([T("任务下载总包命名:"), B("运单资料_【任务编号】.zip"), T("。")]));
|
||||
kids.push(bullet([T("批次文件包命名:"), B("【司机姓名】-【身份证号】-【车牌号】"), T("。")]));
|
||||
kids.push(bullet([T("运单包命名(需求明确):最小序号运单 "), B("【序号】-【运单号】-【货源单号】-含司机及车辆证件"), T(";其余运单 "), B("【序号】-【运单号】-【货源单号】"), T("。")]));
|
||||
kids.push(bullet("运单包内按 合同 / 电子回单 / 司机证件 / 车辆证件 建子目录;文件保留系统原文件名,重名时自动追加 (1)、(2)。"));
|
||||
kids.push(h3("4.4.3 各运单包内容"));
|
||||
kids.push(makeTable(
|
||||
["内容", "最小序号运单", "其余运单", "数据来源"],
|
||||
[
|
||||
["运单相关合同", "√", "√", "运单关联的合同附件(以运单详情-合同附件为准,含承运合同;不含委托合同/框架合同)"],
|
||||
["电子回单", "√", "√", "运单全部支付记录对应的银行电子回单"],
|
||||
["司机证件照片", "√", "×", "该运单司机档案:身份证(人像面/国徽面)、驾驶证、从业资格证等"],
|
||||
["车辆证件照片", "√", "×", "该运单车辆档案:行驶证、道路运输证、车辆照片等"],
|
||||
],
|
||||
[1700, 1400, 1200, 4726],
|
||||
{ centerCols: [1, 2] }
|
||||
));
|
||||
kids.push(spacer(80));
|
||||
kids.push(noteP("证件照片以批次内最小序号运单挂载的司机、车辆档案为准(同批次三要素一致,证件相同,故只随最小序号运单导出一份)。"));
|
||||
kids.push(h3("4.4.4 文件缺失处理"));
|
||||
kids.push(bullet("某运单无合同或无电子回单:不阻断任务,对应子目录不生成,原因记入失败清单(如“无电子回单:运单无支付记录”)。"));
|
||||
kids.push(bullet([T("批次内最小序号运单因故剔除(校验失败/文件缺失):"), B("重新计算"), T("批次内最小序号,证件照片改挂到新的最小序号运单。")]));
|
||||
kids.push(bullet("单个文件下载失败自动重试 3 次,仍失败则记入失败清单,不阻断其余文件。"));
|
||||
|
||||
kids.push(h2("4.5 下载"));
|
||||
kids.push(bullet("任务完成后,任务列表该行显示“下载”按钮,下载内容为任务总包 zip(含全部批次文件包)。"));
|
||||
kids.push(bullet("支持重复下载,不重复生成。"));
|
||||
kids.push(bullet("下载行为记录审计日志(操作人、时间、任务编号)——文件包含身份证等敏感个人信息。"));
|
||||
kids.push(bullet("按批次文件包单独下载:本期不做。"));
|
||||
|
||||
// ── 五、上传示例与生成结果 ──
|
||||
kids.push(h1("五、上传示例与生成结果"));
|
||||
kids.push(para([B("上传清单:")]));
|
||||
kids.push(makeTable(
|
||||
["序号", "货源单号", "运单号", "司机(系统内)", "车牌(系统内)"],
|
||||
[
|
||||
["1", "HY202609001", "YD20260901001", "张三", "皖A12345"],
|
||||
["2", "HY202609002", "YD20260901002", "李四", "皖B67890"],
|
||||
["3", "HY202609001", "YD20260901003", "张三", "皖A12345"],
|
||||
],
|
||||
[800, 2000, 2400, 1900, 1926],
|
||||
{ centerCols: [0, 3, 4] }
|
||||
));
|
||||
kids.push(spacer(80));
|
||||
kids.push(para([B("生成结果:")]));
|
||||
kids.push(bullet([T("批次① = 序号 1、3(张三+同一身份证+皖A12345)→ 文件包“张三-340102199001011234-皖A12345”,内含运单包 "), B("1-…-含司机及车辆证件"), T("(合同+回单+张三证件+皖A12345 证件)和 "), B("3-…"), T("(仅合同+回单)。")]));
|
||||
kids.push(bullet([T("批次② = 序号 2(李四)→ 文件包“李四-340102199001015678-皖B67890”,运单包 "), B("2-…-含司机及车辆证件"), T("(单运单批次,仍含司机及车辆证件)。")]));
|
||||
|
||||
// ── 六、异常与边界场景汇总 ──
|
||||
kids.push(h1("六、异常与边界场景汇总"));
|
||||
kids.push(makeTable(
|
||||
["#", "场景", "处理"],
|
||||
[
|
||||
["1", "模板格式/列名错误", "上传即拒绝"],
|
||||
["2", "序号重复、非正整数", "行级失败清单"],
|
||||
["3", "运单号不存在 / 货源单号不匹配", "行级失败清单"],
|
||||
["4", "运单被关闭、未产生支付记录导致无电子回单", "不阻断,记失败清单"],
|
||||
["5", "运单缺司机或车辆信息无法归批", "行级失败清单"],
|
||||
["6", "批次内最小序号运单被剔除", "证件改挂重新计算后的最小序号运单"],
|
||||
["7", "同一文件重复上传", "允许,各自生成独立任务,不合并去重"],
|
||||
["8", "多任务并发", "后台排队处理(并发上限 1~2),防磁盘/带宽压力"],
|
||||
["9", "文件包超过保留期", "不可下载,需重新上传生成"],
|
||||
["10", "任务处理中系统异常", "任务置“失败”,支持删除后重新上传"],
|
||||
],
|
||||
[500, 3900, 4626],
|
||||
{ centerCols: [0] }
|
||||
));
|
||||
kids.push(spacer());
|
||||
|
||||
// ── 七、非功能需求 ──
|
||||
kids.push(h1("七、非功能需求"));
|
||||
kids.push(makeTable(
|
||||
["项", "要求"],
|
||||
[
|
||||
["异步处理", "打包为后台异步任务,任务列表可见进度(成功/失败条数);上传后无需等待"],
|
||||
["性能", "1000 单规模任务 30 分钟内完成"],
|
||||
["保留期", "生成的文件包默认保留 7 天,过期自动清理"],
|
||||
["敏感数据", "司机身份证号、证件照片属敏感个人信息:菜单需权限控制、下载记审计日志;批次包命名含完整身份证号,包内数据仅限授权人员获取"],
|
||||
["幂等", "同一任务重复点击下载不重复生成文件包"],
|
||||
],
|
||||
[1600, 7426]
|
||||
));
|
||||
kids.push(spacer());
|
||||
|
||||
// ── 八、确认决议记录 ──
|
||||
kids.push(h1("八、确认决议记录(2026-09-08)"));
|
||||
kids.push(para([T("原 v0.1 草稿 13 项待确认事项,经需求方确认"), B("全部采纳文档建议方案"), T(",逐项记录如下:")]));
|
||||
kids.push(makeTable(
|
||||
["#", "事项", "确认结论"],
|
||||
[
|
||||
["1", "菜单名称与挂载位置", "“运单文件包”,挂在运单管理模块下"],
|
||||
["2", "可见角色/权限", "运营人员"],
|
||||
["3", "批次文件包命名", "司机姓名-身份证号-车牌号(含完整身份证号)"],
|
||||
["4", "运单包内子目录", "按 合同/电子回单/司机证件/车辆证件 建子目录"],
|
||||
["5", "“运单相关合同”范围", "以运单详情-合同附件为准(含承运合同),不含委托合同/框架合同"],
|
||||
["6", "货源单号与系统不一致时", "该行失败"],
|
||||
["7", "数据级校验失败行处理", "剔除后继续,任务记“已完成·部分失败”"],
|
||||
["8", "失败行剔除后证件挂载", "挂到批次内剩余运单的最小序号运单"],
|
||||
["9", "行数/大小上限", "2000 行 / 10MB"],
|
||||
["10", "文件包保留期", "7 天"],
|
||||
["11", "按批次包单独下载", "本期不做"],
|
||||
["12", "zip 是否加密", "不加密(依赖权限+审计)"],
|
||||
["13", "序号唯一性范围", "全文件唯一"],
|
||||
],
|
||||
[500, 2800, 5726],
|
||||
{ centerCols: [0] }
|
||||
));
|
||||
kids.push(spacer());
|
||||
|
||||
// ── 九、验收要点 ──
|
||||
kids.push(h1("九、验收要点"));
|
||||
kids.push(numItem("上传合法清单后任务创建成功,完成后可下载 zip;zip 内批次划分与目录命名符合 4.4 规则。"));
|
||||
kids.push(numItem("同一批次内仅最小序号运单包含司机、车辆证件照片,其余运单仅含合同与电子回单。"));
|
||||
kids.push(numItem("单运单批次的运单包仍包含司机及车辆证件照片。"));
|
||||
kids.push(numItem("各类非法数据按 4.2.2 处理,失败清单内容准确可下载。"));
|
||||
kids.push(numItem("任务失败、部分失败、重复下载、保留期过期等边界行为符合第五、六章定义。"));
|
||||
|
||||
// ── 组装文档 ──
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: {
|
||||
document: { run: { font: FONT_BODY, size: 21, color: "333333" } },
|
||||
heading1: { run: { font: FONT_HEAD, size: 30, bold: true, color: "1F3864" }, paragraph: { spacing: { before: 360, after: 180 } } },
|
||||
heading2: { run: { font: FONT_HEAD, size: 25, bold: true, color: "2E5395" }, paragraph: { spacing: { before: 280, after: 140 } } },
|
||||
heading3: { run: { font: FONT_HEAD, size: 22, bold: true, color: "404040" }, paragraph: { spacing: { before: 240, after: 120 } } },
|
||||
},
|
||||
},
|
||||
numbering: {
|
||||
config: [
|
||||
{ reference: "bul", levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 420, hanging: 210 } } } }] },
|
||||
{ reference: "num", levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 420, hanging: 210 } } } }] },
|
||||
],
|
||||
},
|
||||
sections: [{
|
||||
properties: { page: { margin: { top: 1440, bottom: 1440, left: 1440, right: 1440 } } },
|
||||
footers: {
|
||||
default: new Footer({
|
||||
children: [new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ children: ["第 ", PageNumber.CURRENT, " 页 / 共 ", PageNumber.TOTAL_PAGES, " 页"], size: 16, color: "999999" })],
|
||||
})],
|
||||
}),
|
||||
},
|
||||
children: kids,
|
||||
}],
|
||||
});
|
||||
|
||||
const out = path.resolve(__dirname, "..", "requirements", "运单资料文件包下载需求.docx");
|
||||
Packer.toBuffer(doc).then((buf) => {
|
||||
fs.writeFileSync(out, buf);
|
||||
console.log("OK ->", out, buf.length, "bytes");
|
||||
});
|
||||
Reference in New Issue
Block a user