Skip to content

共享组件库 ui/

四个站共用的展示层。apps/* 通过 $ui 别名引用。仓里只有一个 package.json, 因此这是普通目录而非依赖包——不需要 exports 映射,svelte / bits-ui 也天然 只有一份实例。

文档内容
本文定位、目录约定、导入路径、横切模块、样式
contracts.md宿主契约与五条设计准则(读这个才知道为什么组件长这样)
components.md基础件目录:primitives / forms / layout / feedback / data
charts.md图表层:ChartCanvas 架构、8 个图表组件、Chart.js 插件
boards.md复合块:4 个看板、6 个抖动可视化、机群地图

1. 库的定位

收展示,不收行为。 一条判据:组件能不能在只给它 data 的前提下完整渲染?

归库归 app
版式、图表、徽章、表格、地图取数与轮询
组件自有的文案与语义鉴权与令牌
设计令牌与视觉一致性URL 状态、路由
组件自有的交互(排序、翻页、缩放、展开)toast、确认框、错误翻译

因此库里没有 api.ts、没有 poll()、没有 goto()。同一个看板既能挂在控制台, 也能挂在只读的门户上,正是因为它两样都不认识。

2. 目录:按组件类型分,不按使用方分

目录不得出现 control/ / peering/ 这类划分。同一个组件通常多个站都在用, 按站分类第二天就会失效,还会诱导出「control 专用组件」这种伪概念——那正是共享库要 消灭的东西。

目录收什么成员
primitives/原子件IconTooltipIconButtonAsLinkHeadInfo
forms/表单控件SegControlToggleCapCheck
layout/容器与外壳AppShellWidgetCardHead
feedback/状态反馈StatusBadgeHealthBadgeLiveDotEmptyStateInlineBannerSkeletonSkeletonRowsSkeletonTextSkeletonTable
data/表格与分页AsRankTablePagerLoadMore
charts/图表与仪表ChartCanvasTrendChartSparklineBarChartDonutHalfDonutGaugeRateGaugeShareBarChartLegendchartjs.ts
boards/整块仪表盘区块FleetTrafficFleetRoutingAsTrafficFlapStatsPanel
flaps/BGP 抖动可视化PrefixFlapBoardPrefixFlapTableRootCauseBadgeRootCauseSummaryCulpritRefAsPathStripflapZones.ts
map/机群地图与地理FleetMapgeo.ts
styles/设计令牌与全局样式app.css
vendor/第三方 vendored 产物cap/(见 internals/captcha.md

尚未 populated 的目录等对应组件迁进来时再建,不预先留空壳

分层是清晰的:primitives 被所有层用;chartsprimitivesboards / flaps / map 用前面全部。同层之间尽量不互相依赖——AsRankTable 是唯一的例外(它同时用了 WidgetPagerSegControlAsLinkSkeletonRows),因为它本身就是一个成品 表格块。

3. 导入路径

组件从桶文件出

ts
import { Widget, TrendChart, FleetMap, AsRankTable } from '$ui';
import type { NavItem, IconName } from '$ui';

横切模块走各自的子路径,不从桶文件出

ts
import { setUiHost } from '$ui/host.svelte';
import { fmtBytes, relTime } from '$ui/format';
import type { FleetMapNode } from '$ui/types';

分开有两个原因:

  1. 让纯 .ts 模块(例如 app 里的 $lib/format 垫片)不必把 .svelte 组件一并拖进 依赖图;
  2. 避免桶文件的副作用连坐。桶里 re-export 了 CapCheck,它的 ?url 资源 (Cap 的 WASM 与 pako)会被一并 emit 到产物目录——JS 侧摇得掉,文件却留下了。 首页的 fleet 岛为此直接引 $ui/host.svelte 而不走桶文件。

加带 ?url 资源的组件时记住这条:它会影响所有导入 $ui 的产物。

4. 横切模块

host.svelte.ts — 宿主契约

contracts.md

i18n.svelte.ts — 库自带字典

四种语言(en / zh / zh-Hant / ja),覆盖库内组件自有语义的 key (topo.*fstat.*routing.*pflap.*health.*live.*st.*chart.*)。

导出 tUi / lookupUi,供 app 需要与组件共用同一条措辞时使用,避免两份字典改词后 自相矛盾。查不到的 key 回落到宿主字典;再查不到就原样回显 key——可发现的软失败

分工的完整理由见 internals/i18n-and-theming.md

format.ts — 格式化工具

函数用途
parseTs(v)ISO 串或 epoch 整数 → Date | null两种后端都在发,统一从这里进
fmtTime(v)本地时间串
relTime(v)相对时间(「3 分钟前」),走库字典的 rel.*
fmtNum(n)千分位
fmtCompact(n)紧凑记数(13.0k;中日文按万/億)
fmtBytes(n)字节
fmtRate(n)速率(bit/s)
fmtPerSec(n)次/秒
fmtDurationHMS(s)秒 → hh:mm:ss
LIVE_CLSliveness → 徽章色类的映射常量

parseTs 存在的理由是一次真实事故:门户的 TrafficPoint.tepoch 秒, 曾被当成 ISO 串处理——旧图喂 new Date() 不炸但 x 轴显示 1970 年,新图喂 .trim() 当场抛异常、整张图画不出来。时间戳一律经 parseTs

types.ts — 共享数据形状

后端响应的镜像类型,供库组件与 app 共用(FleetMapNodeFleetTrafficBreakdownTrafficByAsFleetPrefixFlapsGroupedFlapZone 等)。口径以 接口契约 为准。

5. 样式

ui/styles/app.css 是设计系统的真身。三个 SvelteKit / Vite 站的 app.css 都只是 @import 它再补自己的私有规则;首页内联了同源的一份。

库里组件的 scoped style 全都假设那套令牌存在--c-data-*--c-ok/warn/bad--fs-*--radius-*--border--text-dim)。缺令牌的表现是「组件渲染成无色」 ——门户此前维护 trimmed 拷贝时就是这样。

令牌全表见 design-tokens.md

app.css 里还有一批全局类是库与 app 共用的锚点:.widget / .badge / .seg / .segbtn / .iconbtn / .pair / .kv / .empty。组件的 scoped style 挂在这些 类上,因此改全局类会同时影响两边。

6. 相关文档