Log 日志
等宽排版的滚动区域,一行一条,可以自动跟随到底部。
用法
root / viewport / content / line 四层;一行写什么由作者决定,组件只提供身份与等宽排版
组件结构
加粗的是必需部件。
data-scope="log":root · viewport · content · line · segment · scroll-to-end-trigger · live-region
示例
按行数定高
rows 决定可见几行,一行的高度归皮肤,修改 --xh-log-line-height 两者一起变化
自动跟随到底部
新行进入时视口自动跟随;向上滚动一段即停止跟随,组件报告的 atBottom 与 scrollToBottom 足以自行绘制一条回到最新
取行中
loading 使日志区报告 aria-busy 并把指针换为忙碌态;正在拉取那一行由作者自行渲染
级别
行上写 level,四档 debug / info / warn / error 由皮肤染色;时间戳与行内标记仍归作者
换为自绘滚动条
视口提供一个 id,用滚动条的 controls 挂载;滚动条浮在内容之上,不占宽度也不留空道
回到底部与播报
向上翻一段,右下角的按钮自动显示,按下后归位并重新粘附;输出结束后在播报区朗读一句结论
ANSI 着色
行上写 ansi 交出带转义的原文,按 SGR 拆成着色的段:颜色映射到语义色,粗体、暗淡、下划线各自生效,其余转义不显示
级别过滤
levels 只显示所选级别的行,用切换按钮组选;没写级别的行不受影响
复制全部
日志旁放一颗剪贴板按钮复制整段输出;带 ANSI 转义的原文先用 stripAnsi 去掉转义,复制出去的是纯文字
虚拟滚动
行数很大时把日志与 Virtualizer 接线:virtualizer 交出 collectionVirtualizer,行放进 Virtualizer 的条目里,只挂窗口里的那些;粘底跟着 Virtualizer 的视口走
设计指引
何时使用
- 构建输出、运行日志、命令行回显。
- 任何从底部持续增长、需要始终跟随到底的内容:内容不需要区分条目身份,整段追加即可。
何时不用
特性
- 结构四层:
root·viewport·content·line;每行内容由作者决定,组件只提供身份与等宽排版。另有两个可选部件:scroll-to-end-trigger与live-region。 rows按行数定高。- 自动跟随到底部;用户向上翻时停止跟随,回到底部后恢复。
- 内置“回到底部”:离开底部时出现,按下后归位并重新粘附。它是 Action Control floating 档的圆钮,比日志的
size低一档、最低 sm(sm / md 时 32px,lg 时 40px)。留空时皮肤绘制向下的字形,放入节点即替换为自定义图形。 - 应用设为
data-material="liquid"时,“回到底部”换成液态面:按下层换色调,按住时液面随手指形变。 - 视口自身可聚焦,整块日志占一个 Tab 停靠位,方向键与翻页键交给浏览器滚动。
- ANSI 着色:行上写
ansi交出带转义的原文(Web Components 在行上写ansi属性、文字就是原文),按 SGR 拆成segment。八种前景色映射到语义色:红、绿、黄、蓝取语气前景,品红、青借代码着色的关键字与字符串色,黑与白取正文与次要前景,每一种都可经--xh-log-ansi-<颜色>覆盖;高亮色(90–97)与基础色同一档。粗体、暗淡、斜体、下划线各自生效;背景色、256 色的高位与真彩色不着色,清行、挪光标之类的转义直接去掉。parseAnsi与stripAnsi同时导出,复制与播报用去掉转义的纯文字。 - 级别过滤:
levels只显示所选级别的行,没写级别的行不受影响;接了虚拟滚动时 DOM 里只有窗口里的行,过滤在交给 Virtualizer 之前按isLevelVisible做。
组合
- 行内可以用文本高亮标出关键词。
- 行数很大时与虚拟滚动接线:
XhVirtualizerRoot包在外面,把它交出的collectionVirtualizer传给virtualizer,日志视口里放 Virtualizer 的视口,行放进 Virtualizer 的条目。日志视口只定高、不滚动,Tab 位与滚动都归里面那层 Virtualizer 视口,粘底跟着它走;这时不用content部件。Web Components 的行写在 Virtualizer 条目里,声明data-xh-part-owner="log"由外层日志认领。 - 复制全部用剪贴板,下载用下载触发器,都放在日志旁边;带转义的原文先经
stripAnsi。 - 给视口一个 id,把滚动条的
controls指向它,滚动条与视口平级放在root内:它浮在内容之上,不占宽度。未挂自绘滚动条时视口自行预留一条通道,原生滚动条出现与消失不会推动文字。
最佳实践
- 用户向上翻时不强行拉回底部。
- 行数很大时接虚拟滚动或截断,不把十万行全部挂载。
反模式
- 每到一行就整块重渲。
- 不提供复制或下载全部日志的入口。
- 把每一行都写进播报区,读屏会被逐行打断。
API 参考
产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-log> |
| Vue 组件 | XhLogContent XhLogLine XhLogLiveRegion XhLogRoot XhLogScrollToEndTrigger XhLogSegment XhLogViewport |
| 组合式函数 | useLog |
| 状态机 | logMachine |
| 皮肤 | @xihan-ui/styles/log.css |
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
threshold | number | 距底部多少 px 视为在底部,默认使用贴底原语的默认值。 | |
virtualizer | CollectionVirtualizer | 与虚拟滚动接线:传入 Virtualizer 的 collectionVirtualizer,行只挂载当前窗口里的那些。 粘底改跟 Virtualizer 的滚动层与内容层,日志视口只保留 role=log 与定高,由它里面的 Virtualizer 视口滚动。 | |
onStickChange | (details: LogStickChangeDetails) => void | 贴底状态变化时通知宿主。 | |
levels | readonly LogLevel[] | 只显示这几个级别的行,缺省全部显示。没写 level 的行不受过滤影响。 接了虚拟滚动时 DOM 里只有窗口里的行,过滤要在交给 Virtualizer 之前按 isLevelVisible 做。 | |
loading | boolean | 行仍在传输中:日志区报告 aria-busy,根写 data-loading。 | |
rows | number | 视口按多少行定高;未提供时高度由皮肤决定。 | |
size | Size | 尺寸:sm / md / lg。影响行文字号与内衬,行高不随档位变化;回到底部钮比它低一档、最低 sm(32 / 32 / 40px)。 | |
translations | Partial<LogTranslations> |
事件
自定义元素将载荷放在 detail;Vue 使用同名 emit。
| 事件 | 载荷 | 说明 |
|---|---|---|
stick-change | LogStickChangeDetails | 贴底状态变化;detail 为 { atBottom: boolean, sticking: boolean } |
插槽
仅列出带载荷的插槽。
| Vue 组件 | 插槽 | 载荷 | 说明 |
|---|---|---|---|
XhLogRoot | default | LogRootSlotProps |
React 适配器 props
只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。
| React 组件 | 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
XhLogLine | level | LogLevel | 该行的级别,写为行上的 data-level。 | |
XhLogLine | ansi | string | 带 ANSI 转义的一行原文:按 SGR 拆成着色的段;写了 children 时以 children 为准。 | |
XhLogRoot | children | SlotChildren<LogRootSlotProps> | ||
XhLogSegment | segment | LogAnsiSegment | 是 | parseAnsi 拆出的一段。 |
状态
公开状态写入 data-state。
| 部件 | 取值 |
|---|---|
scroll-to-end-trigger | 'visible' | 'hidden' |
以下名称仅用于内部状态机。
状态:idle
事件:STICK.CHANGE · SCROLL_TO_BOTTOM · PRESS.START · PRESS.END · TRIGGER.RENDERED
判据:canPress
connect API
getXxxProps() 返回对应部件的宿主属性。
| 成员 | 类型 | 说明 |
|---|---|---|
rows | number | undefined | 取整后的行数;rows 缺席或不是正数时为 undefined。 |
loading | boolean | |
atBottom | boolean | 当前滚动位置是否落在底部阈值内。 |
sticking | boolean | 新行到达时是否自动跟随到底部。 |
showScrollToEndTrigger | boolean | 是否显示回到底部按钮,不在底部时为 true。 |
virtualized | boolean | 是否接了虚拟滚动。 |
isLevelVisible | (level?: LogLevel) => boolean | 某级别的行此刻是否显示;没写级别的行始终显示。 |
scrollToBottom | () => void | 滚动到底部并恢复贴附。 |
getRootProps | () => T['element'] | |
getViewportProps | () => T['element'] | |
getContentProps | () => T['element'] | |
getLineProps | (props?: LogLineProps) => T['element'] | |
getSegmentProps | (segment: LogAnsiSegment) => T['element'] | 一段 ANSI 文字:颜色与字形写成 data 属性,皮肤映射到语义令牌。 |
getScrollToEndTriggerProps | () => T['button'] | |
getLiveRegionProps | () => T['element'] |
无障碍
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Tab | 焦点进入日志区 | 日志区自身可聚焦,方向键/PageUp/PageDown/Home/End 交给浏览器滚动,组件不接管 |
Space / Enter | 焦点在"回到底部"按钮上 | 滚回底部并重新粘附 |
Space / Enter | 按住"回到底部"按钮且视口不在底部 | 按住期间 scroll-to-end-trigger 投影 data-pressed,与指针 :active 同一副按压面;抬起、失焦或回到底部(按钮收起)撤下 |
ARIA
以下属性由 connect 生成。
| 部件 | 属性 | 值 |
|---|---|---|
viewport | aria-busy | 'true' | undefined |
viewport | aria-label | label.log |
viewport | aria-live | 'off' |
viewport | role | 'log' |
scroll-to-end-trigger | aria-label | label.scrollToBottom |
live-region | aria-atomic | 'true' |
live-region | aria-live | 'polite' |
live-region | role | 'status' |
- 视口是
role=log,但其隐含的aria-live被显式关闭:逐行读出连续输出会成为读屏噪声。 - 播报使用独立的
live-region:宿主决定读哪一句、何时读,例如一段输出结束后读出结论与错误条数。不要把每一行原样写入,否则等于重新打开逐行播报。 - 成批取行期间视口报告
aria-busy;播报区是视口的兄弟节点,不受其影响。
样式参考
皮肤
@xihan-ui/styles/log.css 按 [data-scope="log"][data-part="root"] 部件选择器书写,发布产物以挂载类 .xh-scope-log 代替其中的 data-scope(特异性相同),位于 xihan.components 层。覆盖样式使用 xihan.overrides,部件选择器照常可用。
forced-colors: active 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。
数据属性
由 connect 生成;条件不成立时不输出无值属性。
| 部件 | 属性 | 值 |
|---|---|---|
root | data-at-bottom | ''(条件成立时才出现) |
root | data-loading | ''(条件成立时才出现) |
root | data-size | props.size |
root | data-sticking | ''(条件成立时才出现) |
viewport | data-virtualized | ''(条件成立时才出现) |
line | data-level | line?.level |
segment | data-bold | ''(条件成立时才出现) |
segment | data-bright | ''(条件成立时才出现) |
segment | data-dim | ''(条件成立时才出现) |
segment | data-fg | segment.fg |
segment | data-italic | ''(条件成立时才出现) |
segment | data-underline | ''(条件成立时才出现) |
scroll-to-end-trigger | data-pressed | ''(条件成立时才出现) |
scroll-to-end-trigger | data-state | 'visible' | 'hidden' |
scroll-to-end-trigger | data-xh-action-control | '' |
scroll-to-end-trigger | data-xh-action-display | 'always' |
scroll-to-end-trigger | data-xh-action-profile | 'floating' |
scroll-to-end-trigger | data-xh-action-size | floatingSizeBelow(props.size) |
scroll-to-end-trigger | data-xh-action-variant | 'ghost' |
scroll-to-end-trigger | data-xh-liquid | '' |
scroll-to-end-trigger | data-xh-material | 'frosted' |
CSS 变量
本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。
| 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 |
|---|---|---|---|---|---|
--xh-log-ansi-black | segment | color | fg=black | --xh-fg-default | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-blue | segment | color | fg=blue | --xh-fg-info | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-bold-weight | segment | font-weight | bold | --xh-font-weight-semibold | log 的 segment 部件 font-weight 覆盖槽。 |
--xh-log-ansi-cyan | segment | color | fg=cyan | --xh-syntax-string | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-dim-fg | segment | color | dimfgnot([data-fg]) | --xh-fg-subtle | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-green | segment | color | fg=green | --xh-fg-success | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-magenta | segment | color | fg=magenta | --xh-syntax-keyword | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-red | segment | color | fg=red | --xh-fg-danger | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-white | segment | color | fg=white | --xh-fg-muted | log 的 segment 部件 color 覆盖槽。 |
--xh-log-ansi-yellow | segment | color | fg=yellow | --xh-fg-warning | log 的 segment 部件 color 覆盖槽。 |
--xh-log-bg | root | background | default | --xh-bg-surface | log 的 root 部件 background 覆盖槽。 |
--xh-log-border | root | border | default | --xh-border-default | log 的 root 部件 border 覆盖槽。 |
--xh-log-content-px | contentlineviewport | padding-inline | defaultvirtualized | --xh-_log-content-px | log 的 content、line、viewport 部件 padding-inline 覆盖槽。 |
--xh-log-fg | root | color | default | --xh-fg-default | log 的 root 部件 color 覆盖槽。 |
--xh-log-font | contentviewport | font-family | defaultvirtualized | --xh-font-family-mono | log 的 content、viewport 部件 font-family 覆盖槽。 |
--xh-log-font-size | contentviewport | font-size | defaultvirtualized | --xh-_log-font-size | log 的 content、viewport 部件 font-size 覆盖槽。 |
--xh-log-icon-size | scroll-to-end-trigger | --xh-icon-size | default | --xh-_action-profile-glyph-size | log 的 scroll-to-end-trigger 部件 --xh-icon-size 覆盖槽。 |
--xh-log-level-debug-fg | line | color | level=debug | --xh-fg-subtle | log 的 line 部件 color 覆盖槽。 |
--xh-log-level-error-fg | line | color | level=error | --xh-fg-danger | log 的 line 部件 color 覆盖槽。 |
--xh-log-level-info-fg | line | color | level=info | --xh-fg-default | log 的 line 部件 color 覆盖槽。 |
--xh-log-level-warn-fg | line | color | level=warn | --xh-fg-warning | log 的 line 部件 color 覆盖槽。 |
--xh-log-line-height | linerootviewport | block-sizeline-height | default | --xh-text-code-leading | log 的 line、root、viewport 部件 block-size、line-height 覆盖槽。 |
--xh-log-line-px | lineviewport | padding-inline | virtualized | --xh-log-content-px | log 的 line、viewport 部件 padding-inline 覆盖槽。 |
--xh-log-radius | root | border-radius | default | --xh-shape-surface | log 的 root 部件 border-radius 覆盖槽。 |
--xh-log-rows | viewport | block-size | default | 16 | log 的 viewport 部件 block-size 覆盖槽。 |
--xh-log-scroll-to-end-trigger-bg | scroll-to-end-trigger | --xh-ink-surfacebackground-color | defaultdisabledfocus-visiblexh-ink-surface | --xh-_material-bg--xh-_material-bg-focus | log 的 scroll-to-end-trigger 部件 --xh-ink-surface、background-color 覆盖槽。 |
--xh-log-scroll-to-end-trigger-bg-hover | scroll-to-end-trigger | background-color | disabledhoverloadingnot([data-disabled])not([data-loading]) | --xh-_material-bg-hover | log 的 scroll-to-end-trigger 部件 background-color 覆盖槽。 |
--xh-log-scroll-to-end-trigger-border | scroll-to-end-trigger | borderborder-color | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_material-border | log 的 scroll-to-end-trigger 部件 border、border-color 覆盖槽。 |
--xh-log-scroll-to-end-trigger-fg | scroll-to-end-trigger | color | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_material-fg | log 的 scroll-to-end-trigger 部件 color 覆盖槽。 |
--xh-log-scroll-to-end-trigger-inset | scroll-to-end-trigger | inset-block-endinset-inline-end | default | --xh-space-3 | log 的 scroll-to-end-trigger 部件 inset-block-end、inset-inline-end 覆盖槽。 |
--xh-log-scroll-to-end-trigger-radius | scroll-to-end-trigger | border-radius | default | --xh-shape-circle | log 的 scroll-to-end-trigger 部件 border-radius 覆盖槽。 |
--xh-log-scroll-to-end-trigger-shadow | scroll-to-end-trigger | box-shadow | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])is(:hover, [data-loading])loadingnot([data-disabled])not([data-loading])pressed | --xh-_material-shadow | log 的 scroll-to-end-trigger 部件 box-shadow 覆盖槽。 |
--xh-log-scroll-to-end-trigger-size | scroll-to-end-trigger | block-sizeinline-size | defaultxh-action-profile=floating | --xh-_action-profile-visual-size | log 的 scroll-to-end-trigger 部件 block-size、inline-size 覆盖槽。 |
--xh-log-shadow | root | box-shadow | default | none | log 的 root 部件 box-shadow 覆盖槽。 |
--xh-log-tab-size | line | tab-size | default | 4 | log 的 line 部件 tab-size 覆盖槽。 |
动效
动效角色:按压 · 状态 · 出现(见动效规范)。
共享关键帧 xh-overlay-pop-in · xh-pop-out 由 family/motion.css 提供,皮肤 @import 它,单独引入仍成立。时长与缓动读动效令牌,改令牌即改全局节奏。
系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。
RTL
皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像。
