缺 client id 原来整条链路只给 `LoginFailure::Other`:界面画「Sign-in failed / 完整原因在启动器日志里 / Code: other」,用户得翻日志才知道要设 `ULL_MSA_CLIENT_ID`。 落点定在**账户页登录面板**,不是启动流程:这个失败只发生在 `microsoft_auth()`(两条 登录流的入口);没配 id 的发布版建不出微软账户,`identity::resolve` 那条启动路径用户 撞不到(只有「有 id 的构建里登录过、之后 id 被摘掉」才可达)。 - presenter:新增 `LoginFailure::MsaNotConfigured`(`msa_not_configured`)。`microsoft_auth` 改为回 `AuthResult`,`AuthError::Unsupported`(`from_env` 唯一的产出点,零网络)单独成码, 其余失败照旧走 `classify`。诊断原文仍只进日志。 - i18n:账户域新增 `AccountLoginNotConfigured`(标题)/ `AccountFailureMsaNotConfigured` (正文:设 `ULL_MSA_CLIENT_ID=<id>` 或构建时注入,改完重启),golden 同步。 - app-iced:`failure_title_key` 给这一类专属标题;正文按码查表;这一类**不给「重试」** (进程内的环境变量改不了,重试必然是同一个错)。 - 测试:`a_missing_client_id_tells_the_user_what_to_configure` 断言 UI 模型里出现「未配置」 + 变量名 + `<id>` 占位 + `msa_not_configured`,且没有那句「去翻日志」;反向锚 `a_network_failure_does_not_look_like_a_missing_config` 与 `only_a_missing_client_id_becomes_the_config_gap_code` 钉住别的失败不许跟着变。 为什么这一类可以上屏:上屏的是 catalog 固定文案(无凭据、无 token、无内部细节), `DevicePanel::Failed` 里没有 `message`/`detail` 字段,原文在类型上就画不出来; 理由是它的原因本身就是用户的下一步。M117/M119 的「诊断不上屏」约定不动。
99 lines
4.6 KiB
Rust
99 lines
4.6 KiB
Rust
//! 「**模型这一层会给出什么**」的取样器:视图模型字段 + **按码做的 i18n 查表**,拼成一串。
|
||
//!
|
||
//! ## 边界(复审 r2 P2-2 订正,别把这条读成「视图覆盖」)
|
||
//!
|
||
//! 这是**平行实现**:它自己调 `catalog` 查表,**不调用** `login::panel` / `forms::status_line`,
|
||
//! 也不遍历 iced 的元素树。所以它证明的是「模型 + i18n 表里没有中文」,**不证明**「视图真的照它画」。
|
||
//!
|
||
//! 视图层靠两道别的闸:
|
||
//! 1. **类型**:`DevicePanel` / `AuthlibPanel` 里**没有** `message`/`detail` 字段(r3 删掉了),
|
||
//! 想画后端原文是编译错误,不是测试问题;
|
||
//! 2. **出图**:`DevDocs/artifacts/m114/after/*` 由人(tower/复审)逐张看。
|
||
//!
|
||
//! 为什么单独一个文件:`pages/account/tests.rs` 贴着 400 行硬线,而这段是**纯取样**
|
||
//! (不给任何断言)——断言都在 `tests.rs` 里。
|
||
|
||
use unlucky_i18n::{Key, Lang};
|
||
use unlucky_presenter::dto::AccountNoticeCode;
|
||
|
||
use super::login::{failure_body_key, failure_title_key};
|
||
use super::model::{AuthlibPanel, DevicePanel, Hint, Page};
|
||
|
||
/// 页面**会画什么**:模型里的数据 + 视图层对码的 i18n 查表结果。
|
||
pub(super) fn on_screen(page: &Page) -> String {
|
||
let catalog = Lang::En.catalog();
|
||
let mut out = String::new();
|
||
for row in &page.rows {
|
||
out.push_str(&row.name);
|
||
out.push_str(&row.monogram);
|
||
out.push_str(&row.kind_label);
|
||
out.push_str(&row.added);
|
||
out.push_str(&row.detail);
|
||
}
|
||
out.push_str(&page.form.offline_name);
|
||
out.push_str(&page.form.server);
|
||
if let Some(notice) = &page.notice {
|
||
// 与 `widgets::banner` 同源:码 → i18n(主题名拼进去)
|
||
out.push_str(match notice.code {
|
||
AccountNoticeCode::OfflineNameInvalid => {
|
||
catalog.text(Key::AccountNoticeOfflineNameInvalid)
|
||
}
|
||
AccountNoticeCode::AccountExists => catalog.text(Key::AccountNoticeAccountExists),
|
||
AccountNoticeCode::StoreFailed => catalog.text(Key::AccountNoticeStoreFailed),
|
||
AccountNoticeCode::NotFound => catalog.text(Key::AccountNoticeNotFound),
|
||
AccountNoticeCode::Other => catalog.text(Key::AccountNoticeOther),
|
||
AccountNoticeCode::Added => catalog.text(Key::AccountNoticeAdded),
|
||
AccountNoticeCode::Removed => catalog.text(Key::AccountNoticeRemoved),
|
||
AccountNoticeCode::Switched => catalog.text(Key::AccountNoticeSwitched),
|
||
});
|
||
out.push_str(¬ice.subject);
|
||
}
|
||
if let Some(hint) = page.hint {
|
||
out.push_str(match hint {
|
||
Hint::AuthlibReauth => catalog.text(Key::AccountHintAuthlibReauth),
|
||
Hint::OfflineNoRelogin => catalog.text(Key::AccountHintOfflineNoRelogin),
|
||
});
|
||
}
|
||
match &page.device {
|
||
DevicePanel::Hidden => {}
|
||
DevicePanel::Requesting => out.push_str(catalog.text(Key::AccountDeviceRequesting)),
|
||
DevicePanel::Code {
|
||
code,
|
||
link,
|
||
remaining,
|
||
expired,
|
||
} => {
|
||
out.push_str(code);
|
||
out.push_str(link);
|
||
if *expired {
|
||
out.push_str(catalog.text(Key::AccountDeviceExpired));
|
||
} else if let Some(secs) = remaining {
|
||
let clock = format!("{:02}:{:02}", secs / 60, secs % 60);
|
||
out.push_str(&catalog.render(Key::AccountDeviceCountdown, &[("time", &clock)]));
|
||
}
|
||
}
|
||
DevicePanel::Browser { link, .. } => out.push_str(link),
|
||
DevicePanel::Failed { failure } => {
|
||
out.push_str(catalog.text(failure_title_key(*failure)));
|
||
out.push_str(catalog.text(failure_body_key(*failure)));
|
||
out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())]));
|
||
}
|
||
DevicePanel::Succeeded { name } => {
|
||
out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)]));
|
||
}
|
||
}
|
||
match &page.authlib {
|
||
AuthlibPanel::Idle => {}
|
||
AuthlibPanel::Running => out.push_str(catalog.text(Key::AccountServerChecking)),
|
||
AuthlibPanel::Ready { server_name } => out.push_str(server_name),
|
||
AuthlibPanel::Failed { failure } => out.push_str(failure.code()),
|
||
AuthlibPanel::Succeeded { name } => out.push_str(name),
|
||
}
|
||
out
|
||
}
|
||
|
||
/// 有没有中日韩统一表意文字(语言泄漏的最直接判据)。
|
||
pub(super) fn has_cjk(text: &str) -> bool {
|
||
text.chars().any(|c| matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}'))
|
||
}
|