feat(app-iced): M95 日志页接真组件 + 诊断面板(等宽折行 / 按严重度分组 / 一行摘要)
日志页从 M84 占位换成真组件,数据全部来自 presenter 快照(页面不读盘):
- 日志行列表:`pages/logs/lines.rs`,等宽(`BodySmall` 12/16 + mono 族)+ 按词或字形
折行 + 等级色条;**选折行不选横滚**的理由写在模块文档(424px × 200 字符的现实 +
mono 族的 CJK 回退会毁掉横滚唯一的收益「列对齐」)。
- 诊断面板:`pages/logs/diagnostic.rs`,按严重度分组(CRITICAL → WARNING → NOTICE),
每条给规则 id / 结论 / 说明 / 建议动作 / 置信度;卡片头钉住「Copy summary」,
摘要是稳定单行的 `ULL-LOGS v1 | …`(崩溃日志换 issue 时直接粘)。
- 空态:真组件 `feedback::EmptyState`(图标块 + 标题 + 说明 + CTA 回实例页)。
- 两块都在 `pages/logs/panel.rs` 里**定高 + 内部独立滚动**(`FillPortion(3):(2)`):
第一版让诊断卡按内容撑开,三条结论就把日志卡压成 0 高度(实拍复现后改的)。
- 视图模型:`pages/logs/model.rs`(纯展示字段)+ `app/view.rs::logs_model` 做快照转换。
- 接线(只追加):`pages/mod.rs` 的 `pub use logs::…` / `page()` 那行 /
`Message::CopySummary` / `demo_model` 一行;`pages::Model` 加 `logs` 与 `lang` 两个字段;
`app/update.rs` 把 `CopySummary` 落到 `iced::clipboard::write`、`ClearLogs` 清 presenter 的
`log_lines`(呈现层操作,不造命令)。
- i18n:`crates/i18n/src/catalog.rs` 自己锚点下 16 个 key(中英同表);按塔裁决把
`#[cfg(test)] mod tests` 逐字外置到 `crates/i18n/src/catalog/tests.rs`
(catalog.rs 415 → 257 行,tests.rs 164 行;7 个用例逐字未动、i18n 23 个用例全绿)。
验证:`fmt --check` / `clippy -p unlucky-app-iced --all-targets -- -D warnings` 干净;
`test -p unlucky-app-iced` 272 + 6 全绿、`-p unlucky-i18n` 23 全绿;截图
`.screens/logs-{empty,crash,live}-{light,dark}.png`(1180×760,本地 gitignore)。
遗留(已报 finding):`dto::Diagnosis` 缺 core 的 `severity` 与规则表 `cause.key`;
`Action::OpenLogs` 没有对应 `AppCommand`(点了只有占位日志)。
This commit is contained in:
1 parent
c0396544c5
commit
063a1dba39
16 files changed
+1392
-157
No files matched your search
@@ -0,0 +1,159 @@
|
||||
//! 日志行列表:**等宽 + 折行 + 独立滚动**。
|
||||
//!
|
||||
//! ## 为什么折行而不是横向滚动(tower 要求二选一,这里选折行)
|
||||
//!
|
||||
//! 正文列硬上限 424px,而崩溃堆栈动辄 200+ 字符(样例里最长的一行 214 字符)。两条路:
|
||||
//!
|
||||
//! | 方案 | 代价 |
|
||||
//! |---|---|
|
||||
//! | 横向滚动 | 424px 里 200 字符 ≈ 8 屏,读一条 `Caused by:` 要来回甩;外壳已经是竖向滚动,横竖两向嵌套后滚轮归属会打架 |
|
||||
//! | **折行(本页选)** | 一条日志可能占 2~3 行,行号/等级栏必须能标明「这是同一条」 |
|
||||
//!
|
||||
//! 选折行的决定性理由:**折行让等宽族只承担「视觉密度」,不承担「列对齐」**。等宽栈在
|
||||
//! Windows 上是 `Cascadia Mono` / `Consolas`,它们没有 CJK 字形——中文日志要靠 cosmic-text
|
||||
//! 逐字回退到系统 CJK 族(本机实测回退到 `Microsoft YaHei UI`),回退字形的 advance 不是 1ch。
|
||||
//! 横向滚动方案里「第 40 列对齐」是有意义的(堆栈缩进、`at a.b.C(File.java:12)` 的括号列),
|
||||
//! 一旦 CJK 回退把列宽带偏,对齐就废了;折行版面不承诺任何列对齐,回退只影响同一行内的观感。
|
||||
//! 左栏那条 2px 等级条是跨行的(`Length::Fill`),所以「同一条日志折了 3 行」在视觉上仍是
|
||||
//! 一个整体。
|
||||
//!
|
||||
//! ## 等宽族的实际落点
|
||||
//!
|
||||
//! `ui.families().mono` 是启动时用系统字体库核对过的族名([`crate::fonts`] 求值,启动日志里
|
||||
//! 有一行 `mono=…`),本页只通过 [`mono_style`] 用它,不写死族名。字号/行高走
|
||||
//! `TypeRole::BodySmall`(12/16),与报告里的数值表对得上(见 `logs/tests.rs`)。
|
||||
|
||||
use iced::widget::text::LineHeight;
|
||||
use iced::widget::{column, container, row, space};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Pixels};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::{FontFamily, TypeRole, TypeStyle};
|
||||
use unlucky_i18n::catalog::{Catalog, Key};
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::frame;
|
||||
use super::super::Message;
|
||||
use super::model::{Level, Line, Logs};
|
||||
use super::panel;
|
||||
|
||||
/// 等级条宽度(2px:够看见,又不抢日志正文的起始列)。
|
||||
const BAR_WIDTH: f32 = 2.0;
|
||||
|
||||
/// 日志卡片:标题行(`OUTPUT` + 行数)+ 独立滚动的行列表。高度由调用方定(见 [`panel`])。
|
||||
pub fn card<'a>(ui: &'a Ui, logs: &'a Logs, lang: Lang, height: Length) -> Element<'a, Message> {
|
||||
let catalog = Catalog::of(lang);
|
||||
|
||||
let head = row![
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
catalog.text(Key::LogsOutputTitle),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
logs.lines.len().to_string(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center)
|
||||
.padding(Padding::default().bottom(ui.space(Space::X2)));
|
||||
|
||||
let rows = column(
|
||||
logs.lines
|
||||
.iter()
|
||||
.map(|line| entry(ui, line))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill);
|
||||
|
||||
panel::card(ui, height, column![head, panel::scroll(ui, rows)])
|
||||
}
|
||||
|
||||
/// 一行:等级条(跨行)+ 等宽正文(折行)。
|
||||
fn entry<'a>(ui: &'a Ui, line: &'a Line) -> Element<'a, Message> {
|
||||
let color = color_of(ui, line.level);
|
||||
|
||||
let bar = container(space::horizontal())
|
||||
.width(BAR_WIDTH)
|
||||
.height(Length::Fill)
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
border: Border {
|
||||
radius: ui.radius(ShapeScale::Full, BAR_WIDTH),
|
||||
..Border::default()
|
||||
},
|
||||
..container::Style::default()
|
||||
});
|
||||
|
||||
row![
|
||||
bar,
|
||||
mono(ui, &line.text, color)
|
||||
.width(Length::Fill)
|
||||
.wrapping(iced::widget::text::Wrapping::WordOrGlyph),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Start)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 等级 → 颜色(错误用 `Error`,警告用 `Tertiary`:与主题的 warning 槽位同源)。
|
||||
fn color_of(ui: &Ui, level: Level) -> Color {
|
||||
match level {
|
||||
Level::Error => ui.color(Role::Error),
|
||||
Level::Warn => ui.color(Role::Tertiary),
|
||||
Level::Info => ui.color(Role::OnSurface),
|
||||
Level::Debug => ui.color(Role::OnSurfaceVariant),
|
||||
}
|
||||
}
|
||||
|
||||
/// 等宽排版档:**同一个 `BodySmall` 档,只把字体族换成 mono 栈**。
|
||||
///
|
||||
/// 不新造字号(12/16 与辅助说明同档),所以「等宽」这件事只改字体族这一个维度,
|
||||
/// 行高仍是令牌值——CJK 回退只换字形,行高不会跳(这也是折行方案的前提之一)。
|
||||
#[must_use]
|
||||
pub fn mono_style(ui: &Ui) -> TypeStyle {
|
||||
TypeStyle {
|
||||
family: FontFamily::Mono,
|
||||
..ui.style(TypeRole::BodySmall)
|
||||
}
|
||||
}
|
||||
|
||||
/// 等宽文本(字号 / 行高 / 字重 / 族全来自 [`mono_style`])。
|
||||
#[must_use]
|
||||
pub fn mono<'a>(ui: &Ui, content: &'a str, color: Color) -> iced::widget::Text<'a> {
|
||||
mono_styled(ui, content, color)
|
||||
}
|
||||
|
||||
/// 同 [`mono`],但文案是**拥有的**字符串(摘要行这类运行时拼出来的短串)。
|
||||
#[must_use]
|
||||
pub fn mono_owned<'a>(ui: &Ui, content: String, color: Color) -> iced::widget::Text<'a> {
|
||||
mono_styled(ui, content, color)
|
||||
}
|
||||
|
||||
fn mono_styled<'a>(
|
||||
ui: &Ui,
|
||||
content: impl iced::widget::text::IntoFragment<'a>,
|
||||
color: Color,
|
||||
) -> iced::widget::Text<'a> {
|
||||
let style = mono_style(ui);
|
||||
|
||||
iced::widget::text(content)
|
||||
.size(style.size_px)
|
||||
.line_height(LineHeight::Absolute(Pixels(style.line_height_px)))
|
||||
.font(ui.font_of(style))
|
||||
.color(color)
|
||||
}
|
||||
Reference in new issue
Block a user