简介:这份PPT资源面向网络拓扑图绘制人员、售前方案工程师及信息安全从业者,聚焦于绿盟科技风格的图标素材整理,解决Visio等工具中缺少统一规范网络设备图标的痛点。压缩包内仅含1个PPT文件,体积约1.26MB,以幻灯片形式集中呈现各类拓扑图图标,便于直接复制到Visio或PPT中复用。内容涵盖NSFOCUS自有图标、Cisco产品图标(路由器、交换机、集线器、ATM交换机等)、IBM主机与集群控制器、WAN设备、终端与服务器、介质线路、建筑与行业场景等多组分类,并配有目录页与致谢页,结构清晰、检索方便。目前已有910人学习下载,适合需要快速搭建规范拓扑图、统一图例风格或补充绿盟视觉素材的读者参考使用。
1. 一份图标库 PPT 为什么值得当成工程资产来做
很多团队做安全产品界面时,图标都是随手从某个素材站扒一套,改改颜色就上线。结果半年后要加一个新模块,发现原图标库缺对应语义,新图标风格又对不上,最后整个界面像补丁摞补丁。我见过一个做态势感知大屏的项目,前期图标是三个人分别找的,上线评审时被指出「同一排里三个图标线宽都不一样」,返工两周。所以当我看到「常用图标库_绿盟.ppt」这个标题时,第一反应不是「这不就是个素材包」,而是——它其实是一份图标资产的规格说明书:哪些图标、什么语义、什么尺寸、什么配色、怎么命名、怎么交付。把这份 PPT 里的信息拆成可执行的工程规范,才是它真正的价值。这篇笔记面向的是需要维护一套长期可用图标库的前端、UI 和交付工程师,尤其是做安全类产品界面的团队。下面我会按「先立规范、再动手做、最后讲坑」的顺序,把一套图标库从 PPT 变成代码资产的全过程讲清楚。
2. 从 PPT 到图标资产:先定语义分层和命名规则
2.1 为什么图标库必须先做语义分层,而不是先画图
大多数人拿到图标需求,第一件事是打开设计工具开始画。这是最容易翻车的地方。图标库的本质不是「一堆好看的图形」,而是一套语义到图形的映射表。如果语义没定清楚,画得再精致也会在扩展时崩掉。
我一般会把图标按使用场景分成三层:
| 层级 | 用途 | 典型数量 | 变更频率 |
|---|---|---|---|
| 基础操作层 | 增删改查、导入导出、刷新、关闭 | 20~40 | 极低 |
| 状态语义层 | 成功、警告、危险、离线、加载中 | 10~20 | 低 |
| 业务对象层 | 资产、漏洞、告警、策略、日志、拓扑节点 | 30~80 | 中 |
分层的好处是:基础操作层可以跨产品复用,状态语义层跟设计规范绑定,业务对象层允许各产品线自己扩展但必须继承前两层的风格约束。PPT 里如果只给了图标清单没给分层,那这份清单在工程上就是不可维护的——你没法判断新增一个图标该放哪、该谁审批。
命名规则同样要在画图之前定死。常见做法是层级_语义_变体,比如action_search、status_warning_filled、object_asset_server。变体后缀统一用_filled、_outlined、_two-tone三种,不要出现_solid、_line、_duotone混用。这个规则看起来啰嗦,但它是后面自动化脚本能跑起来的前提。
2.2 把 PPT 里的图标清单转成结构化数据
PPT 本身不是给机器读的,所以第一步是把它变成一份结构化清单。我通常用一个 YAML 文件来承载,因为它比 JSON 好写注释,比 CSV 好表达嵌套。
# icons.yaml —— 图标库的唯一数据源 version: "1.0" grid: 24 # 设计栅格 24x24 stroke_width: 1.5 # 描边宽度,filled 变体忽略此项 layers: action: - name: action_search cn: 搜索 variants: [outlined, filled] tags: [查询, 检索] - name: action_export cn: 导出 variants: [outlined] tags: [下载, 输出] status: - name: status_warning cn: 警告 variants: [filled, two-tone] tags: [告警, 注意] object: - name: object_asset_server cn: 服务器资产 variants: [outlined, filled] tags: [主机, 节点]这份 YAML 是整个流程的单一事实来源。后面生成 SVG、生成字体、生成组件、生成文档,全部从它派生。参数说明:grid决定所有图标的画布尺寸,必须统一,否则对齐会出玄学问题;stroke_width只对描边类变体生效,filled 变体在脚本里要跳过这个字段;variants数组决定这个图标要产出几个文件,不要写all这种模糊值,显式列出才能做校验。
2.3 命名校验脚本:在 CI 里拦住不合规的图标
光有规则没用,得让机器来查。下面这个脚本我一般放在仓库的scripts/目录,配合 Git hook 或 CI 跑。
# scripts/validate_icons.py import re import sys import yaml # 命名规则:层级_语义_变体,全小写,下划线分隔 NAME_PATTERN = re.compile(r"^(action|status|object)_[a-z0-9]+(_[a-z0-9]+)*$") VALID_VARIANTS = {"outlined", "filled", "two-tone"} def validate(path): with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) errors = [] seen = set() for layer, items in data.get("layers", {}).items(): for item in items: name = item["name"] # 1. 命名格式校验 if not NAME_PATTERN.match(name): errors.append(f"命名不合规: {name}") # 2. 前缀必须和所属层级一致 if not name.startswith(layer + "_"): errors.append(f"前缀与层级不符: {name} 在 {layer} 下") # 3. 重名校验 if name in seen: errors.append(f"重复命名: {name}") seen.add(name) # 4. 变体合法性 for v in item.get("variants", []): if v not in VALID_VARIANTS: errors.append(f"非法变体: {name} -> {v}") return errors if __name__ == "__main__": errs = validate(sys.argv[1] if len(sys.argv) > 1 else "icons.yaml") if errs: print("\n".join(errs)) sys.exit(1) print("图标清单校验通过")逻辑说明:脚本做了四件事——命名格式、前缀与层级一致性、重名检测、变体白名单。参数上,NAME_PATTERN里的(action|status|object)要和 YAML 里的layers键保持同步,新增层级时两处都要改。失败时看什么:CI 报错会直接打印不合规的图标名,按提示改 YAML 即可,不要绕过校验。这个脚本的价值在于,它把「命名规范」从口头约定变成了硬约束,新人提交图标时不会再靠 reviewer 肉眼盯。
3. 用脚本把图标清单批量产出 SVG 和组件
3.1 从 YAML 生成 SVG 骨架的批量脚本
清单校验通过后,下一步是产出实际图形。设计稿通常是 Figma 或 Sketch 导出的 SVG,但导出文件里往往带着编辑器私有属性、多余的id、未合并的路径。我一般写一个清洗脚本,把设计稿 SVG 统一成规范格式。
# scripts/build_svg.py import os import re import yaml from pathlib import Path SRC_DIR = Path("design/exports") # 设计稿导出目录 OUT_DIR = Path("dist/svg") # 产物目录 # 需要剔除的编辑器私有属性 STRIP_ATTRS = re.compile(r'\s(data-name|id|class|xmlns:xlink)="[^"]*"') def clean_svg(raw: str, size: int, stroke: float) -> str: svg = STRIP_ATTRS.sub("", raw) # 统一画布尺寸和 viewBox svg = re.sub(r'width="[^"]*"', f'width="{size}"', svg, count=1) svg = re.sub(r'height="[^"]*"', f'height="{size}"', svg, count=1) svg = re.sub(r'viewBox="[^"]*"', f'viewBox="0 0 {size} {size}"', svg, count=1) # 统一描边宽度(仅描边类) svg = re.sub(r'stroke-width="[^"]*"', f'stroke-width="{stroke}"', svg) return svg def build(manifest_path: str): data = yaml.safe_load(open(manifest_path, encoding="utf-8")) size, stroke = data["grid"], data["stroke_width"] count = 0 for layer, items in data["layers"].items(): for item in items: for variant in item["variants"]: src = SRC_DIR / f"{item['name']}_{variant}.svg" if not src.exists(): print(f"[缺失] {src}") continue out = OUT_DIR / layer / f"{item['name']}_{variant}.svg" out.parent.mkdir(parents=True, exist_ok=True) out.write_text(clean_svg(src.read_text(encoding="utf-8"), size, stroke), encoding="utf-8") count += 1 print(f"共产出 {count} 个 SVG") if __name__ == "__main__": build("icons.yaml")逻辑说明:clean_svg做三件事——剔除编辑器私有属性、统一画布尺寸、统一描边宽度。参数上,size和stroke直接读 YAML,改一处全局生效。STRIP_ATTRS里的id一定要删,否则多个 SVG 内联到同一页面时会互相覆盖样式,这是最常见的翻车点之一。失败时看什么:如果某个图标报「缺失」,说明设计稿文件名和 YAML 里的name_variant对不上,去design/exports目录核对命名即可。
3.2 生成 React 组件:让前端不再手写 SVG
SVG 有了,但前端直接用<img>引 SVG 会丢失颜色控制能力。常见做法是生成组件,把fill和stroke暴露成 props。
// scripts/gen-component.js const fs = require("fs"); const path = require("path"); const SRC = "dist/svg"; const OUT = "src/components/icons"; // 把 SVG 里的硬编码颜色替换成 currentColor function toComponent(name, svg) { const body = svg .replace(/<svg[^>]*>/, "") .replace(/<\/svg>/, "") .replace(/fill="(?!none)[^"]*"/g, 'fill="currentColor"') .replace(/stroke="(?!none)[^"]*"/g, 'stroke="currentColor"') .trim(); const comp = name.split("_").map(s => s[0].toUpperCase() + s.slice(1)).join(""); return `import React from "react"; export const ${comp} = ({ size = 24, color = "currentColor", ...rest }) => ( <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} {...rest}> ${body} </svg> ); `; } function walk(dir) { for (const f of fs.readdirSync(dir)) { const full = path.join(dir, f); if (fs.statSync(full).isDirectory()) walk(full); else if (f.endsWith(".svg")) { const name = f.replace(".svg", ""); fs.mkdirSync(OUT, { recursive: true }); fs.writeFileSync(path.join(OUT, name + ".jsx"), toComponent(name, fs.readFileSync(full, "utf-8"))); } } } walk(SRC); console.log("组件生成完成");逻辑说明:核心是把 SVG 里的硬编码颜色替换成currentColor,这样组件就能继承父级文字颜色,也支持通过colorprop 覆盖。参数上,size默认 24,和 YAML 里的grid保持一致;viewBox写死0 0 24 24,如果以后改栅格,这里要同步。失败时看什么:如果生成的组件颜色不生效,检查原 SVG 里是不是用了style="fill:#xxx"这种内联样式,正则匹配不到,需要额外处理。
3.3 图标字体方案:什么时候该用,什么时候别碰
有些老项目还在用图标字体(iconfont),因为兼容性好、体积小。但我的血泪经验是:新项目别用图标字体。原因有三个——字体渲染在不同系统上会有亚像素差异,多色图标做不了,无障碍支持差。只有在需要兼容很老的浏览器、或者图标数量极大且都是单色时,才考虑字体方案。
如果确实要做,流程是:SVG 清洗 → 转字体(用常见的字体生成工具)→ 生成 CSS 映射表。映射表要从 YAML 自动生成,不要手写:
# 伪代码示意,具体工具按团队现有链路选 svg2font --input dist/svg --output dist/font/icons.ttf --name icons生成的 CSS 里,每个图标的content码点要和 YAML 里的name建立映射,建议用脚本生成一份icon-map.json,前端通过name查码点,而不是直接写\e601这种魔法数字。这样以后增删图标,前端代码不用改。
4. 图标库落地时的避坑清单
4.1 坑一:同一图标在不同产品里语义漂移
现象:object_asset这个图标,A 产品用来表示「主机资产」,B 产品用来表示「所有资产」,C 产品用来表示「资产分组」。用户跨产品使用时完全懵。
原因:图标库没有语义冻结机制,各产品线按自己理解复用。
解决:在 YAML 里给每个图标加cn和tags字段,并写一份语义说明文档。新增产品线要用某个图标时,必须先在文档里确认语义一致,不一致就新建图标,不要复用。我一般还会在 CI 里加一条检查:如果某个图标的tags被两个以上产品线引用,就提示 review。
4.2 坑二:描边宽度不统一导致视觉重量不一致
现象:一排图标里,有的看起来粗有的看起来细,明明都是 24x24。
原因:设计稿导出时描边宽度没统一,有的 1.5 有的 2,脚本清洗时又只改了stroke-width属性,没处理路径本身的视觉补偿。
解决:在 YAML 里锁定stroke_width,清洗脚本强制覆盖。但要注意,有些图标(比如圆形)在 1.5 描边下会显得比方形细,这是光学错觉,需要在设计阶段做视觉补偿,脚本层面解决不了。常见做法是给圆形类图标单独加optical_compensation字段,清洗时按字段微调。
4.3 坑三:SVG 内联后 id 冲突
现象:页面上同时用了两个图标,其中一个的渐变或裁剪路径失效了。
原因:两个 SVG 里都有id="clip0",内联到同一 DOM 后浏览器只认第一个。
解决:清洗脚本里必须删掉所有id,或者给每个id加文件名前缀。我倾向于直接删,因为图标 SVG 里大部分id都是编辑器自动生成的,删掉不影响渲染。如果确实有clipPath依赖id,就改成id="{name}_clip"这种带前缀的形式。
4.4 坑四:图标清单和实际文件不同步
现象:YAML 里写了 80 个图标,实际只产出 76 个,缺的 4 个没人发现,直到前端用的时候报错。
原因:设计稿交付和清单更新是两条线,没有强制校验。
解决:在构建脚本里加一步「清单 vs 产物」比对,数量对不上直接 fail。下面这段加在build_svg.py末尾即可:
# 构建后校验:清单声明的变体数 vs 实际产出数 expected = sum(len(i["variants"]) for items in data["layers"].values() for i in items) actual = len(list(OUT_DIR.rglob("*.svg"))) assert expected == actual, f"清单声明 {expected} 个,实际产出 {actual} 个"4.5 坑五:颜色硬编码导致暗色模式失效
现象:产品要上暗色模式,图标全是黑的,在深色背景上看不见。
原因:SVG 里写死了fill="#333",组件生成时没替换成currentColor。
解决:组件生成脚本里的颜色替换正则要覆盖fill、stroke、style三种写法。另外,fill="none"和stroke="none"要排除,否则会把不该替换的也换掉。上线前用暗色背景跑一遍视觉回归,比看代码靠谱。
5. 让图标库长期可维护的两个进阶技巧
5.1 用视觉回归测试锁住图标渲染
图标库最怕的是「改了一个图标,影响了另一个」。我一般会引入视觉回归测试:把每个图标渲染成固定尺寸的 PNG,和基线图做像素比对,差异超过阈值就报警。
// 伪代码:基于常见截图比对工具 const { toMatchImageSnapshot } = require("jest-image-snapshot"); expect.extend({ toMatchImageSnapshot }); test("icon visual regression", async () => { for (const name of iconNames) { const png = await renderToPng(name, { size: 48, color: "#000" }); expect(png).toMatchImageSnapshot({ customSnapshotIdentifier: name, failureThreshold: 0.02, // 允许 2% 像素差异 failureThresholdType: "percent", }); } });参数说明:size用 48 而不是 24,是为了放大细节差异,24 像素下很多问题看不出来;failureThreshold设 2%,太低会因抗锯齿差异误报,太高会漏掉真实问题。基线图要提交到仓库,每次改图标后 review 差异图,确认是有意改动再更新基线。
5.2 图标检索页:让设计师和前端自己找图标
图标多了以后,最大的成本是「找」。我一般会从 YAML 自动生成一个静态检索页,支持按名称、中文名、标签搜索,点击可复制组件名或 SVG 代码。
# scripts/gen_gallery.py import yaml, json from pathlib import Path data = yaml.safe_load(open("icons.yaml", encoding="utf-8")) items = [] for layer, lst in data["layers"].items(): for i in lst: items.append({ "name": i["name"], "cn": i.get("cn", ""), "tags": i.get("tags", []), "layer": layer, "variants": i["variants"], }) html = f"""<!DOCTYPE html> <html><head><meta charset="utf-8"><title>图标库</title></head> <body> <input id="q" placeholder="搜索名称/中文/标签" style="width:320px;padding:8px"> <div id="grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:16px;margin-top:16px"></div> <script> const ICONS = {json.dumps(items, ensure_ascii=False)}; const grid = document.getElementById("grid"); function render(list) {{ grid.innerHTML = list.map(i => ` <div style="text-align:center;border:1px solid #eee;padding:12px;border-radius:8px"> <img src="../dist/svg/${{i.layer}}/${{i.name}}_${{i.variants[0]}}.svg" width="32" height="32"> <div style="font-size:12px;margin-top:8px">${{i.cn}}</div> <code style="font-size:11px;color:#888">${{i.name}}</code> </div>`).join(""); }} document.getElementById("q").addEventListener("input", e => {{ const k = e.target.value.toLowerCase(); render(ICONS.filter(i => i.name.includes(k) || i.cn.includes(k) || i.tags.some(t => t.includes(k)) )); }}); render(ICONS); </script> </body></html>""" Path("dist/gallery.html").write_text(html, encoding="utf-8")逻辑说明:脚本从 YAML 读数据,生成一个纯静态 HTML,不依赖任何后端。搜索支持名称、中文名、标签三个维度。参数上,grid-template-columns的minmax(120px, 1fr)控制每列最小宽度,图标多的时候会自动换行。这个页面可以直接部署到内网,设计师和前端自己查,省掉大量「这个图标叫什么」的沟通成本。
5.3 版本管理:图标库也要打 tag
图标库是会被引用的资产,一旦有产品用了某个图标,就不能随便删或改名。我的习惯是:每次发布打一个 git tag,比如icons-v1.2.0;删除或重命名图标必须走 major 版本,并在 CHANGELOG 里写清楚「哪个图标被替换成了哪个」。前端依赖图标库时锁版本,不要用latest。这样即使图标库迭代,老产品也不会突然编译失败。
最后说个我自己的教训:早期做图标库时,我总觉得「规范太麻烦,先画了再说」,结果第三个产品接入时,光是统一命名和描边就花了两周。后来我把 YAML 清单和校验脚本作为第一步,画图反而成了最轻松的部分。图标库这件事,前期多花一天定规则,后期能省一个月返工。希望帮到你。
本文还有配套的精品资源,点击获取