diff --git a/crates/app-iced/src/pages/logs.rs b/crates/app-iced/src/pages/logs.rs index 56e6266..eb7da7d 100644 --- a/crates/app-iced/src/pages/logs.rs +++ b/crates/app-iced/src/pages/logs.rs @@ -51,7 +51,11 @@ mod panel; mod tests; /// 诊断卡在正文里的高度份额(见 [`panel`] 的模块文档:两块必须都是定高 + 内部滚动)。 -const DIAGNOSIS_SHARE: u16 = 3; +/// +/// r2 是 3:2 —— 1180×760 下诊断卡的滚动视口只剩 141px,而一条完整结论(分组标题 → 建议动作 → +/// 置信度)要 177px,于是**首屏看不到任何一条完整结论**(视觉复审 r2 的 P2-A:任务要求的 +/// 「建议动作」在验收尺寸下不可见)。改成 4:2:视口 ~184px,一条结论整块可见。 +const DIAGNOSIS_SHARE: u16 = 4; /// 日志卡的高度份额。 const LOGS_SHARE: u16 = 2; diff --git a/crates/app-iced/src/pages/logs/demo.rs b/crates/app-iced/src/pages/logs/demo.rs index 366fa0c..7bcc654 100644 --- a/crates/app-iced/src/pages/logs/demo.rs +++ b/crates/app-iced/src/pages/logs/demo.rs @@ -34,6 +34,9 @@ pub const STATE_ENV: &str = "ULL_LOGS_FIXTURE"; /// 内嵌的崩溃日志片段(见模块文档的出处)。 const SAMPLE: &str = include_str!("sample_crash.log"); +/// 内嵌的**正常跑完**一场的日志片段(`live` 状态用;见 `sample_live.log` 的文件头)。 +const LIVE_SAMPLE: &str = include_str!("sample_live.log"); + /// 演示状态。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum State { @@ -61,10 +64,14 @@ impl State { pub fn build(self, lang: Lang) -> Logs { match self { Self::Empty => Logs::default().with_status(lang), + // 正常跑完的一场用**自己的**日志样例:页头写「正常结束」却配崩溃日志尾行是自己打自己 + // (视觉复审 r2 的 p3)。 Self::Live => Logs { + instance: Some("开发测试".to_owned()), + when: Some("11-02 19:22".to_owned()), outcome: Some(Outcome::Ok), - findings: Vec::new(), - ..crash() + lines: parse(LIVE_SAMPLE), + ..Logs::default() } .with_status(lang), Self::Crash => crash().with_status(lang), @@ -94,7 +101,7 @@ pub fn crash() -> Logs { } /// 解析内嵌样例:`#` 注释与空行跳过,首字符是等级前缀。 -fn parse(text: &str) -> Vec { +pub(super) fn parse(text: &str) -> Vec { text.lines() .filter_map(|raw| { let raw = raw.trim_end(); @@ -102,17 +109,19 @@ fn parse(text: &str) -> Vec { return None; } - let (prefix, rest) = raw.split_at(1); - let level = match prefix { - "D" => Level::Debug, - "I" => Level::Info, - "W" => Level::Warn, - "E" => Level::Error, + // 按**字符**取等级前缀,不 `split_at(1)`:后者在非 ASCII 首字符上会切到 + // 多字节序列中间直接 panic(视觉复审 r2 的 p3)。 + let mut chars = raw.chars(); + let level = match chars.next() { + Some('D') => Level::Debug, + Some('I') => Level::Info, + Some('W') => Level::Warn, + Some('E') => Level::Error, _ => return None, }; Some(Line { - text: rest.trim_start().to_owned(), + text: chars.as_str().trim_start().to_owned(), level, }) }) diff --git a/crates/app-iced/src/pages/logs/panel.rs b/crates/app-iced/src/pages/logs/panel.rs index 93ab397..64c401d 100644 --- a/crates/app-iced/src/pages/logs/panel.rs +++ b/crates/app-iced/src/pages/logs/panel.rs @@ -10,7 +10,7 @@ //! //! | 块 | 高度 | 为什么 | //! |---|---|---| -//! | 诊断 | `FillPortion(3)` | 崩溃时先读结论;结论通常比日志短,给 3 份 | +//! | 诊断 | `FillPortion(4)` | 崩溃时先读结论;份额要够 177px 的滚动视口,才放得下**一条完整结论**(含建议动作与置信度) | //! | 日志 | `FillPortion(2)` | 日志是长流水,滚就是了 | //! //! 两者都是**兄弟**滚动区(不是嵌套):滚轮归属清楚,不存在「滚动条套滚动条」。 diff --git a/crates/app-iced/src/pages/logs/sample_live.log b/crates/app-iced/src/pages/logs/sample_live.log new file mode 100644 index 0000000..8daf728 --- /dev/null +++ b/crates/app-iced/src/pages/logs/sample_live.log @@ -0,0 +1,19 @@ +# M95 演示数据:一场**正常跑完**的会话(与 sample_crash.log 成对)。 +# +# 为什么单独一份(视觉复审 r2 的 p3):`live` 状态的页头写着「Finished normally」, +# 却配着崩溃日志的尾行(`启动失败:exit code 1`),演示数据自相矛盾。 +# +# 每行格式:`<等级> D|I|W|E <原文>`;`#` 开头是注释。 + +I [19:22:01] [main/INFO] 正在启动 1.20.4,实例「开发测试」,加载器 NeoForge 21.1.72 +I [19:22:02] [main/INFO] Java 21.0.5 (Microsoft build 21.0.5+11-LTS), 内存 4096 MiB, JVM 参数 -Xmx4G -XX:+UseG1GC -Dfile.encoding=UTF-8 -Djava.library.path=C:\Users\Player\AppData\Roaming\.unlucky\instances\dev-test\natives +W [19:22:03] [Render thread/WARN] GLFW 报告 OpenGL 3.2,低于推荐值 4.5;部分光影可能不可用 +I [19:22:05] [Render thread/INFO] OpenGL 3.2.0 NVIDIA 552.44,渲染器 NVIDIA GeForce GTX 1060 6GB +I [19:22:08] [modloading-worker-0/INFO] 已加载 148 个模组,其中 3 个标记为客户端专用:iris-1.7.4, sodium-0.5.11, indium-1.0.32 +I [19:22:14] [Worker-Main-2/INFO] 正在准备生成区域,种子 20261102,生物群系 47 种,结构与地物共 113 项已注册 +I [19:22:19] [main/INFO] 世界「生存世界」已加载,游玩时间 3 小时 12 分 +I [19:22:20] [main/INFO] 本地服务器已启动,端口 25565 +I [19:22:41] [Render thread/INFO] 区块渲染完成,平均帧率 61 fps(垂直同步开) +W [19:31:07] [Render thread/WARN] 一个资源包(Soartex Fanver 1.20)缺少 pack.png,已跳过图标 +I [19:40:12] [main/INFO] 正在停止服务器,保存世界数据 +I [19:40:13] [main/INFO] Stopping! diff --git a/crates/app-iced/src/pages/logs/tests.rs b/crates/app-iced/src/pages/logs/tests.rs index 46c4a44..0911641 100644 --- a/crates/app-iced/src/pages/logs/tests.rs +++ b/crates/app-iced/src/pages/logs/tests.rs @@ -215,6 +215,27 @@ fn empty_state_is_driven_by_both_lists_being_empty() { assert!(live.findings.is_empty(), "live 状态没有诊断"); } +#[test] +fn the_fixture_parser_survives_a_non_ascii_first_char() { + // 抓得住:`parse` 用 `split_at(1)` 切多字节序列会直接 panic(视觉复审 r2 的 p3 实测过)。 + // 抓不住:样例文件本身写坏(比如等级前缀写成小写 `e`)——那会被静默跳过,只体现为行数变少。 + assert!(!demo::parse("日本語の行") + .iter() + .any(|line| line.level == Level::Info)); + + let parsed = demo::parse( + "I 正常一行 +W 警告一行 +# 注释 + +E 错误一行", + ); + assert_eq!(parsed.len(), 3); + assert_eq!(parsed[0].text, "正常一行"); + assert_eq!(parsed[1].level, Level::Warn); + assert_eq!(parsed[2].level, Level::Error); +} + #[test] fn demo_state_comes_from_the_fixture_variable() { // 抓得住:环境变量取值拼错(截图会静默退成 crash,出图的人以为拍到了空态)。