☰
Delphi 组件实现 DOCX 生成与读写:AXWWriter 实战指南
2026/10/17 3:24:40 网站建设 项目流程

简介:面向 Delphi XE10.3 Rio 开发者的 Axolot DOCXSuit 组件套件,包含 AXWWriter、AXWReports 与 DOCXReadWrite 三大部件,专注于在 Delphi 应用程序中创建、编辑和读取 Word DOCX 文件。该套件无需依赖 Microsoft Office,AXWWriter 支持动态生成带文本、段落、表格、图片、页眉页脚的文档,可精细设置字体、字号、颜色、对齐方式、段落样式与页面布局;AXWReports 提供可视化报表设计器,通过拖放完成数据绑定、条件格式化、分组和排序,并能连接数据库生成复杂数据报表;DOCXReadWrite 则能解析已有文档中的节、段落、表格与图像,在保持原有格式与样式的基础上修改保存,支持在大量 DOCX 文件中进行搜索替换和统一格式调整。压缩包大小约三十八点九四兆字节,专为 Delphi XE10.3 Rio 优化兼容性,适合需要集成文档生成、报表输出或批量文档处理功能的 Delphi 中高级开发者使用。目前已有九百五十三人学习下载,这套组件的核心价值在于将底层 DOCX 格式细节封装为易用接口,让开发者把精力集中在业务逻辑上,无论是新建文档还是处理存量文件,都能显著提升交付效率。

1. 从 DOCX 的老式导出说起:这套 Delphi 组件值得拆包吗

拿到这套 Delphi 组件之前,我一直在用 ActiveX 调 Word 的另存接口做 .docx 导出,客户现场机器没装 Office 或 WPS 时整套导出模块直接瘫痪,后来换成 DOCXSuit 里这套以 AXWWriter 为主的纯 Delphi 实现,才把标书和报表导出做成完全不依赖外部程序的独立模块。它本质上不是“转格式工具”,而是一个直接按 OOXML 规范写 ZIP 包文档的 Delphi 类库,适合在 XE10.3 Rio 下做 Win32/Win64 桌面程序的报告生成、模板填充以及既有 DOCX 文件的读取改写。这篇笔记会按“编译安装 → 三个组件分工 → 避坑 → 封装验证”的顺序把可落地的用法写给你,新手能照做,熟手可以直接翻到参数和坑的部分。

2. 安装与包结构:XE10.3 Rio 下的编译顺序和先决条件

2.1 解包后先看什么:目录结构与 DPK 依赖关系

用 7-Zip 解开压缩包后,第一件事不是急着双击 DPK,而是先理清目录结构。常见布局是 src、packages、docs、samples 四个目录,其中 src 存放核心单元,packages 里是 .dpk 和 .dproj,docs 有 CHM 或 PDF 说明,samples 是示例工程。这三个组件不是互相独立的,AXWWriter 和 AXWReports 都建立在 DOCXReadWrite 之上,所以编译顺序必须是底层库在前,业务层在后。我一般会先用资源管理器确认下面的目录对应关系,再把 packages 中的 DPK 按优先级列出来:

{ 伪代码方式列出依赖顺序,方便比对实际目录 } { 1. DOCXReadWrite.dpk -> 封装 ZIP 解压、OOXML 命名空间、document.xml 基础解析 } { 2. AXWWriter.dpk -> 依赖 DOCXReadWrite,提供段落/表格/图片等富文本写接口 } { 3. AXWReports.dpk -> 依赖 AXWWriter 和 DOCXReadWrite,实现模板填充与批量报表 }

把依赖关系先定位好很重要,因为后面如果编译报错,大部分情况都能从“当前 DPK 引用的单元在哪个包”里判断出来。注意这里不是让你照抄我写的包名,而是确认你实际拿到的 DPK 文件有没有明确的分层结构;有的版本为了减少包数量,会把三个组件合成一个 DPK,那么编译顺序的问题就不存在,但如果按三包分发,顺序错了就会遇到找不到 TAXWWriter 这类声明错误。

另外看示例代码时,重点看 samples 里的工程文件用了哪些 uses 子句。你会发现 AXWWriter 单元的名字往往类似 AXW.Writer、AXW.Types、AXW.Utils,而不是一个大而全的单元。这意味着你在自己的工程里也应当按需引用,而不是把整个 src 目录的单元全部 uses 进来,否则 IDE 的编译时间会明显变长,还会引入不必要的 RTL 版本判断逻辑。

2.2 编译安装到 IDE:库路径、设计包与运行包的分离

Delphi XE10.3 Rio 下安装这套组件,标准动作是打开 packages 里的 DPK,在项目管理器中点击 Compile,然后右键 Install。但安装到 IDE 有一个容易被忽略的点:设计期包(Design-Time Package)和运行期包(Runtime Package)要分开放。AXWReports 如果包含报表设计界面,那个带 Design 后缀的 DPK 才需要 Install,纯逻辑部分不要装成设计包,否则 IDE 每次启动都会加载一堆不必要的表单和资源。

安装顺序上,还要先把 src 目录加进 Library Path。在 IDE 的 Tools -> Options -> Environment -> Delphi Options -> Library 里,给 32-bit Windows 平台加上源码目录。这一步不做,编译业务工程时 IDE 会显示找不到某某单元,但奇怪的是直接打开 DPK 编译又正常,原因就是 DPK 自身带相对路径,而你自己的工程没有。

常见的添加参数也一并说清楚:

# 假设解压到 D:\Libs\DOCXSuit,则在 Library Path 中加入: D:\Libs\DOCXSuit\src D:\Libs\DOCXSuit\packages

加完路径后,全局搜索一次单元名,确认没有其他第三方库里的同名单元冲突。之前我遇到过某工具箱里也带了一个 AXWTypes.pas,导致编译时浑水摸鱼用了旧单元,生成出来的文档在 office 里提示“文件损坏”。这个问题很难排查,因为编译不报错,只有打开结果文件时才知道出了问题。所以新环境安装时,务必把 Search Path 里多出来的旧版目录彻底移出。

DXE10.3 还带一个特性:可以用 MSBuild 从命令行编译。如果 IDE 里反复安装失败,我一般会打开“命令行提示符”,切到 packages 目录后执行:

msbuild AXWReports.dproj /p:Config=Debug /p:Platform=Win32 /t:Build

这样能看到完整的错误上下文,IDE 有时只会弹一个模糊的编译错误框。命令行编译也能用来验证当前机器的 RTL 版本是否满足组件的最低要求,因为在编译日志里会直接显示需要哪些 DCU 版本。

2.3 功能开关与条件编译:XE 版本差异导致的常见坑

组件源码里经常出现类似{$IFDEF VER300}或{$IF CompilerVersion >= 32.0}这样的条件编译,用来兼容不同 Delphi 版本。XE10.3 Rio 对应 CompilerVersion 33.0,因此大部分较新的分支都会走到。如果你用的是古董版本的 XE7 或 Delphi 7,某些新 API(例如 System.Zip 里的动态加密支持)会被条件编译直接跳过,功能代码会少一大截。

从实际拆包经验看,这套组件在 DXE10.3 上主要依赖两个基础能力:一个是System.Zip的完整 ZIP 读取能力,另一个是 XML Document Model(MSXML 或 ADOM)的解析器。XE10.3 自带Xml.XMLIntf和Xml.XMLDoc,只要系统里有 MSXML 6.0,就能正常运行。但如果你的部署目标是 Windows Server 精简镜像,MSXML6 可能被禁用,这时哪怕编译通过,运行时也会在读取 document.xml 时抛“类未注册”。解决的办法是在服务器上开启桌面体验功能或安装 MSXML 6 运行库,这个不是组件能替你解决的问题,属于环境准备范围。

另一个条件编译开关是 Unicode 相关。XE10.3 默认的 string 就是 UnicodeString,所以只要组件自身声明都基于string类型,一般不会有编码问题。但如果你把组件用在老项目里,那项目里可能还存在大段 AnsiString 的代码,在给 TAXWRun.Text 赋值前必须做显式转换,否则编译时不会报错,运行时出来的文本却是问号。

我建议在正式用之前,先打开源码目录里 names.txt 或 readme 一类的文件,用文本搜索工具查一遍VER3和UNICODE两个关键字,能看到组件适配过的版本范围。这一步 5 分钟就能做完,却能帮你判断哪些坑是版本差异造成的,避免后面出了问题才开始翻源码。

3. AXWWriter:用代码流式生成 DOCX,而不是去拼接 XML

3.1 为什么不用字符串拼接 XML:docx 本质是一个 ZIP 包

很多人在第一次接触 DOCX 生成时,第一反应是“直接拼 XML 字符串再改后缀”,但 DOCX 根本不是单个 XML 文件,而是一个 ZIP 压缩包,里面至少有[Content_Types].xml、_rels/.rels、word/document.xml、word/_rels/document.xml.rels和样式表。就算你手写 XML 能通过解析校验,ZIP 包里的压缩顺序、内容类型声明、关系文件里的 id 关联也会让你在 Word 和 WPS 之间反复翻车。

AXWWriter 解决的就是这个问题:它把 document.xml 的树形结构映射成一棵对象树,你在代码里操作的是段落、文字、表格,而不是记忆 OpenXML 的复杂标签。它把 ZIP 打包、命名空间声明、关系文件生成全部封装在内部。这套逻辑比自己做字符串拼接要可靠得多,因为组件在保存时会强制刷新核心属性、重新生成关系 id,等于给文档做了一次标准序列化。

从代码视角看,最小写入单元是TAXWWriter下面挂一长串 Paragraph 和 Table 对象。这里要注意,AXWWriter 并不是一个“写一行保存一行”的流式编辑器,它仍然是先构建对象树,最后一次性生成文件。如果你的需求是边读边写几十万字的日志文档,用这个组件反而消耗内存,更适合先拼内容块再统一输出的方式。

3.2 最小可用示例:从空文档到段落、表格、图片

先给一个可编译的最小示例。假设你的窗体上有一个Button1,点击后生成一份包含标题、正文、两行表格的 docx。代码如下:

uses AXW.Writer, AXW.Types; procedure TForm1.Button1Click(Sender: TObject); var Doc: TAXWWriter; Para: TAXWParagraph; Tbl: TAXWTable; ImageRun: TAXWRun; begin Doc := TAXWWriter.Create; try Doc.NewDocument; // 标题段落 Para := Doc.Paragraphs.Add; Para.StyleName := 'Heading1'; Para.AddRun('AXWWriter 测试文档'); // 正文段落 Para := Doc.Paragraphs.Add; Para.AddRun('这是一个通过 Delphi 组件生成的 DOCX 文档。'); // 两行两列表格 Tbl := Doc.Tables.Add(2, 2); Tbl.Cell(0, 0).Text := '姓名'; Tbl.Cell(0, 1).Text := '状态'; Tbl.Cell(1, 0).Text := '某开发者'; Tbl.Cell(1, 1).Text := '已通过验证'; // 插入图片 ImageRun := Para.AddRun; ImageRun.ImagePath := 'C:\Temp\snapshot.png'; ImageRun.Width := 200; ImageRun.Height := 150; Doc.SaveToFile('C:\Temp\result.docx'); finally Doc.Free; end; end;

这个示例里,Doc.Paragraphs.Add返回的是段落对象,AddRun在段落里追加一个文本块;表格用行列坐标直接给单元格赋值,比手工写 XML 直白得多。SaveToFile会自己处理 ZIP 打包,你不需要手动压缩,也不需要显式写[Content_Types].xml。注意图片是放到Para这个段落里,示例代码里沿用了上一个正文段落变量,这意味着图片会出现在上面那段文字后面,如果你希望在独立段落里插入图片,应该重新Add一个新段落再AddRun。

参数说明里,ImagePath是绝对路径时组件内部会读取文件流,生成的 docx 不会引用原路径,而是把图片二进制打包进word/media/目录。但要注意,图片路径不存在时组件不会立即报错,只会在最终打开文件时缺图,所以代码里用绝对路径时最好先做FileExists判断。

3.3 关键对象与生命周期:对象树、引用计数与释放顺序

AXWWriter 的对象模型是典型父子树,TAXWWriter是根节点,Paragraphs、Tables是中间层,段落里的 Run、表格里的 Cell 是叶子节点。在使用时你会注意到,Para.AddRun返回的值如果赋给局部变量后直接Free,会导致整棵对象树析构时重复释放。这个组件内部大量使用了接口引用计数,所有从对象树上获取的子对象都属于树的一部分,不应该单独释放。

释放顺序上,我一般只释放根节点Doc.Free,不手动释放 Paragraph 或 Table。如果你发现某次生成后出现“Cannot access a freed object”的异常,十有八九是你在中途把某个节点赋给全局变量,后续又FreeAndNil了一次。正确做法是给节点对象设置引用后再还给树,或者干脆不要在局部变量里长期保存节点。

再看样式设置。Para.StyleName := 'Heading1'在组件内部会往 styles.xml 里追加一个 Heading1 样式定义,但如果你的模板文档里已经存在同名样式,组件可能会覆盖它。为了避免互相干扰,生成新文档前尽量用Doc.NewDocument初始化,不要沿用旧文档对象反复生成多份内容。实验后发现,连续调用两次NewDocument后,第二次生成的文件大小会比第一次小一点,原因是样式表被重置了,这不算异常,但能证明对象树确实被清理干净了。

这块还有一个容易踩的边界:在生成过程中调用Doc.Paragraphs.Count拿到的数量,和最终文档里实际段落数不一定一致。原因是部分空 Run 在序列化时会被组件优化掉,比如AddRun('')不会出现在最终文件里。如果你依赖段落索引做后续处理,千万不要假设索引和内容一一对应。

4. AXWReports 与 DOCXReadWrite:模板报表和既有文件的读写边界

4.1 AXWReports:基于模板的批量报表思路

AXWReports 的设计思路和纯代码生成不同,它倾向于“先有模板,再填值”。你可以用 Word 或 WPS 先画一份带占位符的 .docx 模板,把标题、表格边框、字体都排好,然后在 Delphi 代码里加载模板、替换占位符、输出多份报表。这样做的价值在于:如果最终用户频繁调整页面边距或表头样式,你只需要改模板文件,不用重编译程序。

使用上有一个典型阈值要记住:模板里的占位符越少、结构越固定,AXWReports 的替换效率越高;如果模板中有二十层嵌套表格,建议先把模板拆成几段再合并。我这边的做法是保留一个最小模板,里面对应插入区域用单独标记,比如{{CUSTOMER_NAME}}、{{ORDER_ITEMS}},然后在 Delphi 里用代码循环填行。示例结构如下:

uses AXW.Reports; var Rep: TAXWReport; ItemList: TStringList; I: Integer; begin Rep := TAXWReport.Create; try Rep.LoadTemplate('C:\Templates\report_template.docx'); Rep.SetFieldValue('CUSTOMER_NAME', '某公司'); Rep.SetFieldValue('GENERATE_DATE', DateToStr(Now)); ItemList := TStringList.Create; try ItemList.Add('第一项'); // 在模板标记处循环插入表格行 ItemList.Add('第二项'); for I := 0 to ItemList.Count - 1 do Rep.AddTableRow('ORDER_ITEMS', ['名称', ItemList[I]]); finally ItemList.Free; end; Rep.Generate('C:\Output\report_' + FormatDateTime('yyyymmddhhnnss', Now) + '.docx'); finally Rep.Free; end; end;

这段代码里,SetFieldValue做的是文本替换,AddTableRow是模板表格追加行。要注意,占位符替换基于文本匹配,对大小写敏感,而且要求占位符在模板里是独立运行的文本,不能被 Word 自动拆分成多个 XML 节点。如果你在 Word 里输入占位符后又做了拼写检查,有时 Word 会把一个单词拆成两段标签,导致组件替换失败。最稳的办法是在模板里用格式干净的纯文本,不要开启自动更正或断字。

4.2 DOCXReadWrite:读取、查找、替换的边界

DOCXReadWrite 这个组件一般用来处理已经存在的文档,比如批量替换合同里的甲方名称、把旧版本里的表格数据导出来。它和 AXWReports 不同,不要求模板有规范的占位符,而是直接对文本和段落做查找替换。但这里有个边界要注意:DOCXReadWrite 对复杂 OOXML 的还原度并不是百分之百,如果原文档里有动态字段、页眉页脚里的复杂标识、或者内嵌对象,处理完后这些内容可能丢失或错位。

在读取场景中,最典型的用法是先加载文档、遍历段落、收集文本,再按条件改写。示例:

uses DOCX.ReadWrite; var Reader: TAXWDocxReader; Paths: TStringList; I: Integer; Text: string; begin Reader := TAXWDocxReader.Create; Paths := TStringList.Create; try Reader.LoadFromFile('C:\Docs\origin.docx'); Reader.ListParagraphs(Paths); // 把所有段落文本放入列表 for I := 0 to Paths.Count - 1 do begin Text := Paths[I]; if Pos('旧公司名', Text) > 0 then Reader.ReplaceText(Format('p%d', [I]), '旧公司名', '新公司名'); end; Reader.SaveToFile('C:\Docs\replaced.docx'); finally Paths.Free; Reader.Free; end; end;

这里的ListParagraphs返回的是段落原文,ReplaceText的第一个参数是段落索引标识,第二个和第三个参数分别是旧文本和新文本。用索引做替换有个坑:一旦替换过一段,后面的段落索引在内部存储里可能发生偏移,所以替换顺序建议从文档末尾往前处理,或者先收集所有要改的段落,最后统一替换。另外,ReplaceText默认替换第一处匹配,不是全局替换;如果需要全文替换,要么循环多次,要么看组件版本有没有ReplaceAll参数,否则容易漏内容。

4.3 三个组件的分工与内存模型差异

从使用边界看,三个组件的分工很清晰:DOCXReadWrite 解决读和基础写,适合文本级操作;AXWWriter 解决从零创建富文本文档,适合代码化生成;AXWReports 解决模板化批量报表,适合格式固定、内容变化的场景。它们之间不是重复造轮子,而是面向不同的使用频率和模板复杂度。

内存模型上也有差异。DOCXReadWrite 因为要解析已有文件,会一次性把 XML 内容加载进对象树,大小较大的文件(几十 MB)会比较吃内存;AXWWriter 因为是自底向上构建内容,理论上更节省,但频繁调用对象方法也有较多临时对象产生。AXWReports 则介于两者之间:模板解析按需加载,但生成多页重复表头时会持有更多临时引用。

一个实际经验是:不要同时加载两份以上的大文档做内容拼接,DOCXReadWrite 对多文档同时操作的支持不太好,容易互相覆盖全局状态。我还是复用同一个 Reader 实例,处理完一份就Free掉再创建新的,这比尝试复用流程稳得多。

5. 避坑与常见问题:编译报错、乱码和文件打不开的排查

这一章的每一条都是实际拆包时遇到的真实问题,按“现象 → 原因 → 解决”的结构写,方便你出问题时快速定位。建议把这一章当成收藏夹里的排错手册,遇到异常了再翻到这里对号入座。

5.1 现象一:编译报错 MSB3061,找不到 DPK 文件

现象:打开 packages 下的 DPK 后编译,提示类似MSB3061: Unable to find 'DOCXReadWrite.dpk' in search path,但你在资源管理器里明明能看到文件存在。

原因:DPK 文件的内部引用是相对路径,而编译时的当前目录是工程文件所在目录。如果你把 DPK 复制到了另一个目录,或用了符号链接打开工程,Delphi 计算相对路径时就找不到依赖的另一个包。本质上不是文件丢失,而是路径基准不一致。

解决:不要直接复制 DPK 到自己的工程目录,统一打开 packages 原目录;如果必须复制,把它的兄弟 DPK 文件一起复制,保持目录结构不变。另一种方式是直接在 IDE 的 Library Path 里加入 packages 目录,让编译器能跨目录搜索到所有依赖单元。

5.2 现象二:生成的文件在 WPS 打开正常,Word 打开提示“文件已损坏”

现象:同一份result.docx,WPS 能打开,微软 Word 会弹提示说文件损坏需要修复,点“是”之后又能正常显示内容。

原因:Word 对 OOXML 的校验比 WPS 严格。比较常见的原因是生成的document.xml里存在无效的空白文本节点,或者单元格的<w:tcPr>顺序不标准。AXWWriter 在多数版本下生成的文档是规范的,但如果你手动设置过StyleName或ImagePath,组件对样式 ID 的引用可能没写进 styles.xml,Word 发现悬空引用后会判定文件损坏。

解决:先用 7-Zip 打开生成的 docx,解出word/document.xml,用 Notepad++ 检查有没有w:style="undefined"或空<w:r></w:r>节点。如果确实有,说明你引用了模板里不存在的样式名;把StyleName换成组件内置的默认值(如Normal)或手动追加样式。另外,生成图片时使用的Width和Height数值要是奇数,Word 对某些插件写的图片尺寸会有兼容问题,我一般统一改为偶数。

5.3 现象三:中文内容变成问号或乱码

现象:输出的 docx 里英文正常,中文全部变成???或一堆乱码符号。

原因:docx 内部的 XML 以 UTF-8 存储,但组件里的赋值代码用 AnsiString 传入了中文字符串,导致内容被按错误代码页转换。X E10.3 默认是 Unicode,问题往往出在外部数据上,比如从文件编码为 GBK 的文本里读取了内容,直接塞进组件。组件拿到的是非法 Unicode 序列,序列化时自然无法正确编码。

解决:所有进入组件的中文字符串统一转成 UTF-8:TEncoding.UTF8.GetString(TFile.ReadAllBytes(...))。然后检查字体设置,Word 中文默认使用的字体是中易宋体,如果 OOXML 里的字体设置只写了ascii="Arial"没写eastAsia,Word 会用默认西文字体渲染中文,看起来像乱码。给段落设置字体时,要同时设置FontName对应的 east-Asia 字体,具体接口可以在组件里搜EastAsiaFont或FontEastAsia属性。

5.4 现象四:内存异常,提示 Cannot access a freed object

现象:程序运行第一次生成文档正常,第二次点击按钮生成时直接卡死或抛出Invalid pointer operation,但单步调试时第一次却没有问题。

原因:这是接口引用计数和类引用混用的典型表现。AXWWriter 的部分Add方法返回的是接口,你如果把它赋值给类类型变量并在局部释放,引用计数归零时机就不受对象树控制,第二次执行时对象已经释放但树里还持有旧引用。

解决:不要到处保存中间节点,而是链式调用:Doc.Paragraphs.Add.AddRun('文本'),让返回值在临时引用作用域内自然赋值。如果必须保存节点,用接口变量类型声明,比如var Par: IAXWParagraph,而不是TAXWParagraph。从我自己踩出的习惯看,凡是遇到用了两次就崩的问题,先检查是不是把所有子对象都当作类来FreeAndNil了,这是最常见的翻车点。

6. 进阶:把套件包进自己的工具类与验证产物

6.1 封装一个文档生成服务类

把 AXWWriter 直接写在按钮事件里,短期能跑,但长期维护会很痛苦。我更推荐做一层薄封装,把常用流程统一收敛,比如创建文档、追加标题、追加表格、保存文件。这样业务代码里不用每次引用组件单元,以后换库也只需要改服务内部。

type TDocxBuilder = class private FDoc: TAXWWriter; public constructor Create; destructor Destroy; override; procedure AppendHeading(Level: Integer; const Text: string); procedure AppendParagraph(const Text: string); procedure SaveToFile(const FileName: string); end; constructor TDocxBuilder.Create; begin FDoc := TAXWWriter.Create; FDoc.NewDocument; end; destructor TDocxBuilder.Destroy; begin FDoc.Free; inherited; end; procedure TDocxBuilder.AppendHeading(Level: Integer; const Text: string); var P: TAXWParagraph; begin P := FDoc.Paragraphs.Add; P.StyleName := Format('Heading%d', [Level]); P.AddRun(Text); end;

这个封装里要注意两点:一是构造函数里自动调用NewDocument,避免外部忘记初始化;二是保存后在Destroy里统一释放,外部不需要感知组件生命周期。业务调用时只需要Builder.SaveToFile(...),错误处理也更集中。

6.2 用 OpenXML SDK 或 7-Zip 验证产物

生成完文档不要直接拿 Word 打开看一眼就完事,我习惯再用 7-Zip 解一次包,检查word/document.xml中关键内容是否真的存在。更深一层是用 OpenXML SDK 的DocumentFormat.OpenXml.Packaging打开生成的 docx,来校验是否是合规的 OpenXml 包。没有 SDK 也没关系,用命令行解压document.xml然后搜索文本即可:

7z e result.docx word/document.xml -o. -y

看到document.xml里出现你填的中文、图片引用路径、表格单元格文本,就说明内容序列化成功了。从那以后我每次用新组件都会强制走一遍“编译 → 最小样例生成 → 7z 解包验证 → 双编辑器打开”四步流程,再复杂的报表生成也就不容易翻车。这套 Delphi 组件套件在 XE10.3 Rio 下能解决项目里大部分 DOCX 生成与读写诉求,希望这组验证习惯也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询