ShiErFeng/shierfeng-fastapi-backend/cli/tui/widgets/status_panel.py
2026-07-24 09:43:10 +08:00

324 lines
8.7 KiB
Python

from textual.widgets import Static
from cli.tui.copy import TUI_COPY
class StatusPanelRenderingSupport:
"""
状态面板渲染支持对象。
该对象负责状态类名解析、结构化正文渲染和信号带脉冲文本构建,
供状态面板、指标卡与信号带组件复用。
"""
PULSE_FRAMES = ['·', '', '', '']
@staticmethod
def resolve_status_class(status: str) -> str:
"""
根据状态值解析样式类。
:param status: 面板状态
:return: 状态样式类名
"""
normalized_status = str(status).strip().lower()
if normalized_status in {'ok', 'success', 'healthy'}:
return 'is-ok'
if normalized_status in {'fail', 'error', 'down'}:
return 'is-fail'
if normalized_status in {'warn', 'warning', 'degraded'}:
return 'is-warn'
return 'is-info'
def render_structured_body(self, body: str) -> str:
"""
将结构化正文文本渲染为更有层次的终端块内容。
支持以下约定:
- `## ` 开头:分组标题
- `> ` 开头:说明或子项
- 空字符串:保留空行
:param body: 原始正文文本
:return: 渲染后的正文文本
"""
rendered_lines: list[str] = []
for raw_line in body.splitlines():
stripped = raw_line.strip()
if not stripped:
if rendered_lines and rendered_lines[-1] != '':
rendered_lines.append('')
continue
if stripped.startswith('## '):
title = stripped[3:].strip()
if rendered_lines and rendered_lines[-1] != '':
rendered_lines.append('')
rendered_lines.append(f'{title}')
rendered_lines.append('' * max(10, min(len(title) * 2, 28)))
continue
if stripped.startswith('> '):
rendered_lines.append(f'{stripped[2:].strip()}')
continue
rendered_lines.append(f'{stripped}')
while rendered_lines and rendered_lines[0] == '':
rendered_lines.pop(0)
while rendered_lines and rendered_lines[-1] == '':
rendered_lines.pop()
return '\n'.join(rendered_lines) if rendered_lines else f'- {TUI_COPY.build_status_panel_empty_text()}'
def build_signal_rail_text(self, lines: list[str], pulse_index: int) -> str:
"""
构建信号带渲染文本。
:param lines: 信号文本行
:param pulse_index: 当前脉冲帧索引
:return: 渲染文本
"""
pulse = self.PULSE_FRAMES[pulse_index % len(self.PULSE_FRAMES)]
body = '\n'.join(line for line in lines if line.strip())
return TUI_COPY.build_signal_rail_text(pulse, body or TUI_COPY.build_signal_rail_empty_text())
STATUS_PANEL_RENDERING = StatusPanelRenderingSupport()
class StatusPanel(Static):
"""
TUI 状态面板组件。
:param title: 面板标题
:param status: 面板状态
:param body: 面板正文
"""
DEFAULT_CSS = """
StatusPanel {
border: double #245874;
padding: 1 2;
margin: 0;
height: 17;
min-height: 17;
background: #08131f;
color: #def8ff;
}
StatusPanel.is-ok {
border: double #22c983;
background: #081b17;
}
StatusPanel.is-fail {
border: double #ff6b7a;
background: #241118;
}
StatusPanel.is-warn {
border: double #f2b84b;
background: #261d0f;
}
StatusPanel.is-info {
border: double #38d8ff;
background: #081825;
}
StatusPanel:focus {
border: heavy #6df7ff;
background: #10364b;
}
"""
def __init__(
self,
title: str,
status: str,
body: str,
rendering: StatusPanelRenderingSupport | None = None,
) -> None:
"""
初始化状态面板。
:param title: 面板标题
:param status: 面板状态
:param body: 面板正文
:param rendering: 状态面板渲染支持对象
:return: None
"""
self.title = title
self.status = status
self.body = body
self.rendering = rendering or STATUS_PANEL_RENDERING
super().__init__(self._build_render_text(), markup=False)
self.can_focus = True
self.add_class(self.rendering.resolve_status_class(self.status))
def _build_render_text(self) -> str:
"""
构建面板渲染文本。
:return: 渲染文本
"""
rendered_body = self.rendering.render_structured_body(self.body)
normalized_status = TUI_COPY.render_status_label(self.status)
return TUI_COPY.build_status_panel_text(
self.title,
TUI_COPY.render_status_code(self.status),
normalized_status,
rendered_body,
)
class MetricPanel(Static):
"""
TUI 首页驾驶舱指标卡组件。
:param title: 指标标题
:param value: 指标主值
:param status: 指标状态
:param hint: 指标补充说明
"""
DEFAULT_CSS = """
MetricPanel {
border: double #245874;
padding: 1 2;
height: 12;
min-height: 12;
background: #08131f;
color: #effcff;
}
MetricPanel.is-primary {
border: heavy #6df7ff;
height: 12;
min-height: 12;
background: #0a1725;
text-style: bold;
}
MetricPanel.is-ok {
border: double #22c983;
background: #081b17;
}
MetricPanel.is-fail {
border: double #ff6b7a;
background: #241118;
}
MetricPanel.is-warn {
border: double #f2b84b;
background: #261d0f;
}
MetricPanel.is-info {
border: double #38d8ff;
background: #081825;
}
"""
def __init__(
self,
title: str,
value: str,
status: str,
hint: str,
*,
accent: bool = False,
rendering: StatusPanelRenderingSupport | None = None,
) -> None:
"""
初始化指标卡。
:param title: 指标标题
:param value: 指标主值
:param status: 指标状态
:param hint: 指标补充说明
:param accent: 是否作为主指标高亮展示
:param rendering: 状态面板渲染支持对象
:return: None
"""
self.title = title
self.value = value
self.status = status
self.hint = hint
self.rendering = rendering or STATUS_PANEL_RENDERING
super().__init__(self._build_render_text(), markup=False)
self.add_class(self.rendering.resolve_status_class(self.status))
if accent:
self.add_class('is-primary')
def _build_render_text(self) -> str:
"""
构建指标卡渲染文本。
:return: 渲染文本
"""
normalized_status = TUI_COPY.render_status_label(self.status)
return TUI_COPY.build_metric_panel_text(
self.title,
self.value,
TUI_COPY.render_status_code(self.status),
normalized_status,
self.hint,
)
class SignalRail(Static):
"""
首页信号带组件,用于展示简洁的系统态势条目。
:param lines: 信号文本行
"""
DEFAULT_CSS = """
SignalRail {
border: double #1db9d1;
padding: 1 2;
margin-top: 1;
margin-bottom: 1;
min-height: 6;
background: #06111b;
color: #e9fdff;
}
"""
def __init__(self, lines: list[str], rendering: StatusPanelRenderingSupport | None = None) -> None:
"""
初始化首页信号带。
:param lines: 信号文本行
:param rendering: 状态面板渲染支持对象
:return: None
"""
self.lines = lines
self._pulse_index = 0
self.rendering = rendering or STATUS_PANEL_RENDERING
super().__init__(self._build_render_text(), markup=False)
def on_mount(self) -> None:
"""
挂载后启动轻量脉冲刷新。
:return: None
"""
self.set_interval(0.8, self._advance_pulse, name='signal rail pulse')
def _advance_pulse(self) -> None:
"""
推进信号带脉冲状态。
:return: None
"""
self._pulse_index = (self._pulse_index + 1) % 4
self.update(self._build_render_text())
def _build_render_text(self) -> str:
"""
构建信号带渲染文本。
:return: 渲染文本
"""
return self.rendering.build_signal_rail_text(self.lines, self._pulse_index)