产品目录、检查清单、操作步骤、条款摘要,这类内容在 PDF 里用列表呈现最清楚。手工在编辑器里一条条敲还算可行,但条目一旦要跟着数据走——比如把一份分类表、一份待办清单导出成 PDF,手工排版就跟不上了:条目数量、编号顺序、缩进层级都得随数据生成。
本文介绍用 Spire.PDF for JavaScript 实现无序列表、有序列表与多级列表,其中无序列表分内置符号与图片符号两种。它基于 WebAssembly 在浏览器端直接创建与保存 PDF 文档,全过程在本地完成,通过虚拟文件系统(VFS)读写文件,无需后端配合。
本文介绍四个核心功能点:
列表类型不同,用到的类和标记对象也不同,对照下表选择:
| 列表类型 | 列表类 | 标记配置 | 常用取值 |
|---|---|---|---|
| 无序列表(内置符号) | PdfList |
PdfMarker + PdfUnorderedMarkerStyle |
Disk、Square、Circle、Asterisk |
| 无序列表(图片符号) | PdfList |
PdfMarker 的 image 参数(样式自动变为 CustomImage) |
任意图片,按文字行高缩放 |
| 有序列表(编号) | PdfSortedList |
PdfOrderedMarker + PdfNumberStyle(Suffix 改编号后缀,StartNumber 改起始编号) |
Numeric、LowerLatin、UpperLatin、LowerRoman、UpperRoman |
| 多级列表(嵌套) | PdfList / PdfSortedList + 列表项的 SubList |
各级列表各自的 Marker 与 Indent |
一级与子级可各自取不同的符号或编号 |
有关安装和项目配置,请参考 React 项目中集成 Spire.PDF for JavaScript。以下示例默认已安装 Spire.PDF 并完成 WebAssembly 模块初始化。
在 PDF 页面中创建无序列表
Spire.PDF for JavaScript 提供 PdfList 创建列表,通过 PdfUnorderedMarkerStyle 枚举类型切换符号形状。
function App() {
const createBulletLists = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将中文字体载入 VFS,供列表文字使用
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 列表字体与条目内容
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 12 });
const items = ['果汁饮料', '调味品', '糖果点心', '乳制品', '谷物与麦片', '肉类与禽类', '果蔬', '海产品'];
// 第一份列表:逐项添加,项目符号用 Square
const list = new pdfModule.PdfList({ font: font });
for (const item of items) {
list.Items.Add(item);
}
list.Marker = new pdfModule.PdfMarker({ style: pdfModule.PdfUnorderedMarkerStyle.Square });
// Brush 同时作用于项目符号与列表文字
list.Brush = new pdfModule.PdfSolidBrush({ pdfRGBColor: new pdfModule.PdfRGBColor({ color: pdfModule.Color.get_Navy() }) });
list.Indent = 10;
list.TextIndent = 6;
const first = list.Draw({ page: page, x: 0, y: 40 });
// 第二份列表接着第一份的底部绘制,项目符号改用 Circle
const list2 = new pdfModule.PdfList({ font: font });
for (const item of items) {
list2.Items.Add(item);
}
list2.Marker = new pdfModule.PdfMarker({ style: pdfModule.PdfUnorderedMarkerStyle.Circle });
list2.Brush = pdfModule.PdfBrushes.get_Black();
list2.Indent = 10;
list2.TextIndent = 6;
list2.Draw({ page: page, x: 0, y: first.Bounds.Bottom + 20 });
// 保存并从 VFS 读回,触发下载
const outputFileName = '项目符号列表.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>创建无序列表</h1>
<button onClick={createBulletLists}>
开始创建
</button>
</div>
);
}
export default App;
同一份内容用 Square 与 Circle 两种项目符号创建的无序列表:

在 PDF 页面中用图片创建无序列表
Spire.PDF for JavaScript 还提供 PdfImage 读取图片,把它交给 PdfMarker 即可作为项目符号,符号样式自动变为 CustomImage。
function App() {
const createImageBulletList = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将中文字体与作项目符号的图片载入 VFS
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
await window.spire.FetchFileToVFS('logo.png', '', `${process.env.PUBLIC_URL}/data/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 列表字体与条目内容
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 12 });
const items = ['果汁饮料', '调味品', '糖果点心', '乳制品', '谷物与麦片', '肉类与禽类', '果蔬', '海产品'];
// 读取图片交给 PdfMarker 作符号,样式自动变为 CustomImage
const image = pdfModule.PdfImage.FromFile('logo.png');
const marker = new pdfModule.PdfMarker({ image: image });
// 逐项添加条目并套用图片符号
const list = new pdfModule.PdfList({ font: font });
for (const item of items) {
list.Items.Add(item);
}
list.Marker = marker;
list.Brush = pdfModule.PdfBrushes.get_Black();
list.Indent = 10;
list.TextIndent = 6;
list.Draw({ page: page, x: 0, y: 40 });
// 保存并从 VFS 读回,触发下载
const outputFileName = '图片符号列表.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>用图片创建无序列表</h1>
<button onClick={createImageBulletList}>
开始创建
</button>
</div>
);
}
export default App;
以 logo.png 作项目符号创建的无序列表:

在 PDF 页面中创建有序列表
有序列表由 PdfSortedList 创建,编号随条目自动递增,编号形式由 PdfOrderedMarker 上的 PdfNumberStyle 枚举类型决定。
function App() {
const createOrderedLists = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将中文字体载入 VFS,供列表文字使用
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 列表字体与条目内容
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 12 });
const items = ['果汁饮料', '调味品', '糖果点心', '乳制品'];
// 第一份列表:阿拉伯数字编号,编号后缀默认是 "."
const list = new pdfModule.PdfSortedList({
marker: new pdfModule.PdfOrderedMarker({ style: pdfModule.PdfNumberStyle.Numeric, font: font }),
});
list.Font = font;
for (const item of items) {
list.Items.Add(item);
}
list.Indent = 12;
list.TextIndent = 6;
list.Brush = pdfModule.PdfBrushes.get_Black();
const first = list.Draw({ page: page, x: 0, y: 40 });
// 第二份列表:大写罗马数字编号,编号后缀改为 "、"
const marker = new pdfModule.PdfOrderedMarker({ style: pdfModule.PdfNumberStyle.UpperRoman, font: font });
marker.Suffix = '、';
const list2 = new pdfModule.PdfSortedList({ marker: marker });
list2.Font = font;
for (const item of items) {
list2.Items.Add(item);
}
list2.Indent = 12;
list2.TextIndent = 6;
list2.Brush = pdfModule.PdfBrushes.get_Black();
list2.Draw({ page: page, x: 0, y: first.Bounds.Bottom + 20 });
// 保存并从 VFS 读回,触发下载
const outputFileName = '编号列表.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>创建有序列表</h1>
<button onClick={createOrderedLists}>
开始创建
</button>
</div>
);
}
export default App;
阿拉伯数字与大写罗马数字两种编号方式创建的有序列表

在 PDF 页面中创建多级列表
多级列表靠条目上的 SubList 属性实现,把子列表挂到某个条目上就多出一级,缩进由子列表自己的 Indent 决定。
function App() {
const createMultilevelList = async () => {
// 获取 Spire.PDF WASM 模块
const pdfModule = window.wasmModule?.spirepdf;
// 检查模块是否就绪
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// 将中文字体载入 VFS,供列表文字使用
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 创建 PDF 文档并添加一个空白页面
const doc = new pdfModule.PdfDocument();
const page = doc.Pages.Add();
// 列表字体与分组数据
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 12 });
const groups = [
{ title: '饮料与乳品', children: ['果汁饮料', '乳制品'] },
{ title: '生鲜与谷物', children: ['谷物与麦片', '果蔬'] },
{ title: '肉类与海鲜', children: ['肉类与禽类', '海产品'] },
];
// 一级列表:阿拉伯数字编号
const root = new pdfModule.PdfSortedList({
marker: new pdfModule.PdfOrderedMarker({ style: pdfModule.PdfNumberStyle.Numeric, font: font }),
});
root.Font = font;
root.Indent = 10;
root.TextIndent = 6;
root.Brush = pdfModule.PdfBrushes.get_Black();
// 每个一级条目下挂一个子列表,子列表用 Disk 项目符号并缩进 18 磅
for (const group of groups) {
const item = root.Items.Add(group.title);
const sub = new pdfModule.PdfList({ font: font });
for (const child of group.children) {
sub.Items.Add(child);
}
sub.Marker = new pdfModule.PdfMarker({ style: pdfModule.PdfUnorderedMarkerStyle.Disk });
sub.Indent = 18;
sub.TextIndent = 6;
sub.Brush = pdfModule.PdfBrushes.get_Black();
item.SubList = sub;
}
// 整份多级列表从页面顶端开始绘制
root.Draw({ page: page, x: 0, y: 40 });
// 保存并从 VFS 读回,触发下载
const outputFileName = '多级列表.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>创建多级列表</h1>
<button onClick={createMultilevelList}>
开始创建
</button>
</div>
);
}
export default App;
编号一级条目与项目符号子列表组成的两级列表

常见问题
列表项的文字没有显示出来
原因:列表条目用的是 Font 属性上的字体。没有设置它,或者字体文件还没有读进虚拟文件系统时,页面上只会留下项目符号,文字(尤其是中文)不会出现。
解决:先把字体文件载入 /Library/Fonts/,再把字体对象交给列表的 Font:
// 字体文件读入虚拟文件系统
await window.spire.FetchFileToVFS('ARIAL UNICODE MS.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
// 用该文件创建字体,在构造列表时传入(构造后再赋给 list.Font 效果相同)
const font = new pdfModule.PdfTrueTypeFont({ fontFile: '/Library/Fonts/ARIAL UNICODE MS.TTF', size: 12 });
const list = new pdfModule.PdfList({ font: font });
创建列表时报 "Ambiguous call"
原因:PdfList、PdfSortedList、PdfMarker 都有多个重载,位置传参无法判断该走哪一个,抛出 Ambiguous call: arguments (object) match multiple overloads.,并在报错里列出可选的键名。list.Draw(page, x, y) 这种位置传参同样会被拒绝。
解决:改用对象表示法,把参数名显式写出来:
// 报错写法:new pdfModule.PdfList(font)
const list = new pdfModule.PdfList({ font: font });
const sortedList = new pdfModule.PdfSortedList({ marker: marker });
const marker2 = new pdfModule.PdfMarker({ style: pdfModule.PdfUnorderedMarkerStyle.Disk });
// 绘制同理:list.Draw(page, 0, 40) 报错,改成
list.Draw({ page: page, x: 0, y: 40 });
用图片作项目符号时报 "IO_FileNotFound_FileName"
原因:PdfImage.FromFile 读的是虚拟文件系统里的路径,图片没有先用 FetchFileToVFS 读进去时就会抛 IO_FileNotFound_FileName——图片存在磁盘上不算数。
解决:先把图片读进虚拟文件系统,再用同一个文件名读取:
// 图片读入虚拟文件系统
await window.spire.FetchFileToVFS('logo.png', '', `${process.env.PUBLIC_URL}/data/`);
// 用同一个文件名读取
const image = pdfModule.PdfImage.FromFile('logo.png');
const marker = new pdfModule.PdfMarker({ image: image });
获取免费许可证
如果您希望删除结果文档中的评估消息,或者摆脱功能限制,请该Email地址已收到反垃圾邮件插件保护。要显示它您需要在浏览器中启用JavaScript。获取有效期 30 天的临时许可证。







