fix(app-iced, presenter): M114 r3 —— 登录成功提示走 i18n key+参数,离线名字提交判据与 host 同源

- 新增 Key::AccountLoginSucceeded(Signed in as {name} / 已登录 {name}):
  presenter 不再拼「已登录:{name}」这类人类可读句子(复审 r1 P1-1 的收尾)
- valid_offline_name 从 host 搬到 dto(UI 与 host 共用的同一个判据),
  Form::offline_ready 改用它:按钮不再「亮着但必然失败」
- 视图模型测试:成功提示按 i18n 渲染进 on_screen;离线提交判据加「太短也拒绝」断言
This commit is contained in:
AskaEth committed 2026-11-05 05:09:52 +08:00
1 parent 8d8552113a
commit 9d0d985b1b
9 files changed
+38 -21

No files matched your search

+3 -1
View File
@@ -315,7 +315,9 @@ fn failed<'a>(ui: &'a Ui, failure: LoginFailure, catalog: Catalog) -> Element<'a
/// 成功态。
fn succeeded<'a>(ui: &'a Ui, name: &'a str, catalog: Catalog) -> Element<'a, PageMessage> {
let text = format!("{} · {name}", catalog.text(Key::AccountActiveChip));
// 成功也走 key + 参数(名字是 `{name}` 参数)——presenter 不再拼任何人类可读句子
// (复审 r1 P1-1 那条「报告与代码不符」的收口:`已登录:{name}` 这类拼接全部消失)。
let text = catalog.render(Key::AccountLoginSucceeded, &[("name", name)]);
let body = column![
frame::text_role_owned(
ui,
+3 -1
View File
@@ -152,7 +152,9 @@ impl Form {
/// 本地档案名是否已填(`trim` 后非空才算)。
#[must_use]
pub fn offline_ready(&self) -> bool {
!self.offline_name.trim().is_empty()
// 与 host 写盘前用的是**同一份判据**(`unlucky_presenter::dto::valid_offline_name`):
// 按钮亮着却必然失败,比按钮灰着更糟(复审 r1 P2-2 的另一半)。
unlucky_presenter::dto::valid_offline_name(self.offline_name.trim())
}
/// 第三方站表单是否已填(三项都非空;地址的语法问题由 host 的规范化兜)。
+13 -7
View File
@@ -4,9 +4,7 @@
//! |---|---|
//! | `a_jwt_injected_into_events_never_reaches_the_page` | **凭据红线**:DTO → 状态 → 视图模型这一路漏了脱敏 |
//! | `a_failure_with_a_provider_code_renders_english_and_the_code` | **语言泄漏(r1 p1)**:后端中文诊断串又直接上屏了 |
//! | `an_empty_account_list_offers_all_three_tracks` | 空态变成白页(没有入口) |
//! | `the_device_code_panel_shows_a_countdown_and_expires` | 设备码「只转圈」/ 倒计时不是人话格式 |
//! | `a_third_party_account_falls_back_to_the_monogram` | 没有皮肤却画白块 / 拿假皮肤充数 |
use unlucky_i18n::{Key, Lang};
use unlucky_presenter::dto::{
@@ -98,16 +96,15 @@ fn on_screen(page: &Page) -> String {
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(name),
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(catalog.text(failure_body_key(*failure)));
out.push_str(failure.code());
}
AuthlibPanel::Failed { failure, .. } => out.push_str(failure.code()),
AuthlibPanel::Succeeded { name } => out.push_str(name),
}
out
@@ -352,6 +349,15 @@ fn the_form_tracks_are_independent() {
form.open(Track::Offline);
assert_eq!(form.panel, Panel::Offline);
assert!(!form.offline_ready(), "空名字不能提交");
form.offline_name = "ab".to_string();
assert!(
!form.offline_ready(),
"太短也不许提交(与 host 同一条规则)"
);
assert!(
!unlucky_presenter::dto::valid_offline_name("ab"),
"UI 判据 = host 那份规则"
);
form.offline_name = "Alex".to_string();
assert!(form.offline_ready());
+1
View File
@@ -90,6 +90,7 @@ macro_rules! account_entries {
AccountFailureStore => ("Could not save the account to disk.", "账户没能写到磁盘上。"),
AccountFailureOther => ("The sign-in failed. The full reason is in the launcher log.", "登录失败,完整原因在启动器日志里。"),
AccountDeviceRequesting => ("Contacting Microsoft…", "正在联系微软…"),
AccountLoginSucceeded => ("Signed in as {name}", "已登录 {name}"),
// ── 账户页横幅(添加/删除/切换的结果;页面上真实可见,不是 toast)
AccountNoticeAdded => ("Added {name}", "已添加 {name}"),
AccountNoticeRemoved => ("Removed {name}", "已删除 {name}"),
+1
View File
@@ -370,6 +370,7 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("AccountFailureStore", "Could not save the account to disk.", "账户没能写到磁盘上。"),
("AccountFailureOther", "The sign-in failed. The full reason is in the launcher log.", "登录失败,完整原因在启动器日志里。"),
("AccountDeviceRequesting", "Contacting Microsoft…", "正在联系微软…"),
("AccountLoginSucceeded", "Signed in as {name}", "已登录 {name}"),
("AccountNoticeAdded", "Added {name}", "已添加 {name}"),
("AccountNoticeRemoved", "Removed {name}", "已删除 {name}"),
("AccountNoticeSwitched", "{name} will be used for the next launch", "{name} 将作为下次启动的账户"),
+11
View File
@@ -76,6 +76,17 @@ impl AccountView {
}
}
/// 离线名字的**界面契约**(3–16 位 ascii 字母/数字/下划线)。
///
/// 放在 `dto` 而不是 host 里:它是 UI(提交按钮能不能点)与 host(写盘前的最后一道校验)
/// 共用的**同一个判据**——两处各写一遍就会出现「按钮亮着但必然失败」的坑。
/// core 侧还有一道同名规则做兜底(`offline_meta`),这里只管界面这条。
#[must_use]
pub fn valid_offline_name(name: &str) -> bool {
let len = name.chars().count();
(3..=16).contains(&len) && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
/// 一张皮肤原图。
///
/// **停在「原图」这一层是刻意的**:presenter 不引入图片解码依赖(`presenter-no-ui` +
+3 -3
View File
@@ -29,9 +29,9 @@ mod settings;
mod versions;
pub use accounts::{
looks_like_jwt, redact, AccountKind, AccountNotice, AccountNoticeCode, AccountView,
AuthlibLoginView, AuthlibRequest, DeviceLoginPhase, DeviceLoginView, LoginFailure, SkinImage,
REDACTED,
looks_like_jwt, redact, valid_offline_name, AccountKind, AccountNotice, AccountNoticeCode,
AccountView, AuthlibLoginView, AuthlibRequest, DeviceLoginPhase, DeviceLoginView, LoginFailure,
SkinImage, REDACTED,
};
pub use agent::{AgentEvent, ChatMessage, ChatRole, ConfirmRequest, Sensitivity, ToolCallView};
pub use content::{
+3 -2
View File
@@ -21,7 +21,8 @@ use unlucky_core::download::blocking::block_on;
use crate::dto::CoreEvent;
use crate::dto::{
AccountKind, AccountNotice, AccountNoticeCode, AccountView, DeviceLoginView, LoginFailure,
valid_offline_name, AccountKind, AccountNotice, AccountNoticeCode, AccountView,
DeviceLoginView, LoginFailure,
};
use super::env::HostEnv;
@@ -33,7 +34,7 @@ pub mod authlib;
mod failure;
pub use failure::{valid_offline_name, AccountFailure};
pub use failure::AccountFailure;
use failure::classify;
@@ -33,13 +33,6 @@ impl AccountFailure {
}
}
/// 离线名字的界面契约(3–16 位 ascii 字母/数字/下划线)。core 侧还有一道同名规则做兜底。
#[must_use]
pub fn valid_offline_name(name: &str) -> bool {
let len = name.chars().count();
(3..=16).contains(&len) && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
/// `AuthError` → 原因码(**只看变体与 ascii 码,不看中文文案**)。
pub(super) fn classify(error: &AuthError) -> LoginFailure {
match error {