fix(app-iced, presenter): M114 r2 —— 登录失败走结构化错误码 + i18n,修 4 条 p2 与 3 条看图意见
p1(语言泄漏):后端诊断串不再上屏
- presenter 新增 LoginFailure 原因码(denied / whitelist_pending / unreachable /
not_authlib_server / bad_credentials / no_profile / store_failed / other)
- 页面按码查 i18n 出标题+正文+「Code: …」,原文只进日志/详细信息
- 进行中(Requesting)也走 i18n,不再复读后端中文状态串
- presenter/state 不再发中文 toast(成败由页面按 i18n 画)
- 新增守门测试:注入带 AADSTS 的失败事件,断言英文视图无 CJK 且含 code
p2-1 复制:on_account 返回 Task,page_messages 把 iced::clipboard::write 交回 runtime
p2-2 可见失败态:新增 AccountNotice(added/removed/switched/名字非法/重名/落盘失败/
找不到),页面用 M3 内联 alert 画(不是 toast);离线名字先按 3–16 位规则校验,
失败不再清空输入
p2-3 重新登录按账户类型分派:微软→授权码流;第三方→预填服务器/登录名并提示补密码;
离线→明确说明没有可重登的服务器账户
p2-4 入口收敛:删 chip 行;空态=三卡片;有账号时三卡收进 Add an account(按钮不再绑死微软轨)
看图意见/tower 三条:
- 失败横幅改成 M3 alert(SurfaceContainerHigh + 描边,errorContainer 只作图标块,
不再通栏实心红);色值本身经复审像素采样确认是 90/30 档,未改角色
- 当前账户卡补齐与列表一致的「服务器/登录名」行;无皮肤时明说 No skin available
- 页面级 scrollable 加 8px 右 gutter,滚动条不再压卡片
- 倒计时改 mm:ss(15:00 left),数字与冒号语言无关
p3(顺带):AuthlibRequest / Form 手写 Debug 脱敏密码;HTTP 响应体与 base64 输出加上限;
离线提示文案与行为对齐;删除无人使用的 key;皮肤测试断言帽子层真的 blend
测试:app-iced lib 418 绿、presenter 151 / i18n 24 绿(workspace 全量另跑)
This commit is contained in:
1 parent
1ff00bbe50
commit
8d8552113a
31 files changed
+1540
-497
No files matched your search
@@ -7,24 +7,38 @@
|
||||
//! 2. **密码只走一次**:`Submit` 把 `AuthlibRequest` 交给后端后立刻清掉表单里的密码
|
||||
//! (成功失败都清——重试用新输入,不在内存里多留一份凭据);
|
||||
//! 3. **表单态是纯 UI 状态**:输入框改动只写 `App::account_form` + 置脏,不派命令。
|
||||
//!
|
||||
//! ## 返回 `Task`(M114 r2 / 复审 P2-1)
|
||||
//!
|
||||
//! 复制用户码/链接必须把 `iced::clipboard::write` 的 `Task` **交回 runtime**——iced 0.14 的
|
||||
//! `Task` 是惰性 stream,丢掉返回值等于永不执行(日志页的 Copy summary 就是正确写法)。
|
||||
|
||||
use unlucky_presenter::dto::{AppCommand, AuthlibRequest};
|
||||
use iced::Task;
|
||||
use unlucky_presenter::dto::{AccountKind, AppCommand, AuthlibRequest};
|
||||
|
||||
use crate::pages::account::{Message as Account, Panel, Track};
|
||||
use crate::app::Message;
|
||||
use crate::pages::account::{Hint, Message as Account, Panel, Track};
|
||||
|
||||
use super::super::state::App;
|
||||
|
||||
impl App {
|
||||
/// 账户页消息。
|
||||
pub(super) fn on_account(&mut self, message: Account) {
|
||||
pub(super) fn on_account(&mut self, message: Account) -> Task<Message> {
|
||||
// 复制是唯一需要**交回 Task** 的分支:先把它的 Task 抓出来,其余分支照旧改状态
|
||||
let clipboard = match &message {
|
||||
Account::Copy(text) => Some(text.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match message {
|
||||
Account::Open(track) => {
|
||||
self.account_form.open(track);
|
||||
// 微软面板一打开就把「设备码」这条路准备好:用户点开就该看到码或选择按钮,
|
||||
// 而不是再点一次「登录」(面板里的两条流按钮仍然可选)
|
||||
if track == Track::Microsoft && self.state.device_login.phase.visible() {
|
||||
self.account_form.panel = Panel::Microsoft;
|
||||
}
|
||||
self.account_form.hint = None;
|
||||
self.view_dirty = true;
|
||||
}
|
||||
Account::ToggleAdd => {
|
||||
self.account_form.section_open = !self.account_form.section_open;
|
||||
self.account_form.hint = None;
|
||||
self.view_dirty = true;
|
||||
}
|
||||
Account::Close => {
|
||||
@@ -35,6 +49,7 @@ impl App {
|
||||
}
|
||||
self.view_dirty = true;
|
||||
}
|
||||
Account::DismissNotice => self.dispatch(AppCommand::DismissAccountNotice),
|
||||
Account::Server(value) => {
|
||||
self.account_form.server = value;
|
||||
self.view_dirty = true;
|
||||
@@ -59,15 +74,14 @@ impl App {
|
||||
}
|
||||
Account::Submit => match self.account_form.panel {
|
||||
Panel::Offline => {
|
||||
// 名字**不清空**:失败时用户还要看见自己输入的(复审 r1 P2-2 的另一半)
|
||||
let name = self.account_form.offline_name.trim().to_string();
|
||||
if !name.is_empty() {
|
||||
self.account_form.offline_name.clear();
|
||||
self.dispatch(AppCommand::AddOfflineAccount(name));
|
||||
}
|
||||
}
|
||||
Panel::Authlib => {
|
||||
let ready = self.account_form.authlib_ready();
|
||||
if ready {
|
||||
if self.account_form.authlib_ready() {
|
||||
let request = AuthlibRequest::new(
|
||||
self.account_form.server.clone(),
|
||||
self.account_form.login.clone(),
|
||||
@@ -76,7 +90,7 @@ impl App {
|
||||
self.dispatch(AppCommand::AddAuthlibAccount(request));
|
||||
}
|
||||
}
|
||||
Panel::Microsoft => self.on_account(Account::StartBrowser),
|
||||
Panel::Microsoft => return self.on_account(Account::StartBrowser),
|
||||
Panel::None => {}
|
||||
},
|
||||
Account::StartDevice => self.dispatch(AppCommand::StartDeviceLogin),
|
||||
@@ -85,26 +99,62 @@ impl App {
|
||||
self.account_form.close();
|
||||
self.dispatch(AppCommand::CancelDeviceLogin);
|
||||
}
|
||||
Account::Copy(text) => {
|
||||
// 剪贴板只放用户码/链接这类**公开**内容(凭据永不经过这条路径)
|
||||
let _: iced::Task<crate::app::Message> = iced::clipboard::write(text);
|
||||
}
|
||||
Account::Copy(_) => {}
|
||||
Account::Switch(id) => self.dispatch(AppCommand::SwitchAccount(id)),
|
||||
Account::Remove(id) => self.dispatch(AppCommand::RemoveAccount(id)),
|
||||
Account::Relogin => {
|
||||
// 「重新登录」按微软轨的默认流走(授权码 + 系统浏览器)
|
||||
Account::Relogin(id) => return self.relogin(&id),
|
||||
}
|
||||
self.refresh_view();
|
||||
|
||||
match clipboard {
|
||||
Some(text) => iced::clipboard::write(text),
|
||||
None => Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 「重新登录」按**账户类型**分派(复审 r1 P2-3)。
|
||||
///
|
||||
/// - 微软轨:走授权码流(原路);
|
||||
/// - 第三方轨:第三方站没有「一键重登」——预填服务器地址与登录名,让用户补密码;
|
||||
/// - 本地档案:没有服务器账户可重登 ⇒ 明确说明,不假装能做。
|
||||
fn relogin(&mut self, id: &str) -> Task<Message> {
|
||||
let Some(account) = self
|
||||
.state
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| account.id == id)
|
||||
.cloned()
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
self.account_form.hint = None;
|
||||
match account.kind {
|
||||
AccountKind::Microsoft => {
|
||||
self.account_form.panel = Panel::Microsoft;
|
||||
self.dispatch(AppCommand::StartBrowserLogin);
|
||||
}
|
||||
AccountKind::Authlib => {
|
||||
self.account_form.open(Track::Authlib);
|
||||
self.account_form.server = account.server_url.clone();
|
||||
self.account_form.login = account.login_name.clone();
|
||||
self.account_form.password.clear();
|
||||
self.account_form.hint = Some(Hint::AuthlibReauth);
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
}
|
||||
AccountKind::Offline => {
|
||||
self.account_form.hint = Some(Hint::OfflineNoRelogin);
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
}
|
||||
}
|
||||
self.refresh_view();
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
/// 取证开关 `ULL_ACCOUNT_STATE` 的落地:把「很难自然等到」的状态钉死一次。
|
||||
/// 取证开关 `ULL_ACCOUNT_STATE` 的落地(**只在 `--screenshot` 下生效**)。
|
||||
///
|
||||
/// 为什么需要:设备码过期要等 15 分钟、第三方站表单要手点两下,截图通道等不到。这里只在
|
||||
/// **截图模式**下生效(`--screenshot`),交互模式一律不读——与 `ULL_START_ROUTE` 同一口径。
|
||||
/// 为什么需要:设备码过期要等 15 分钟、表单要手点两下,截图通道等不到自然发生的状态。
|
||||
pub(super) fn force_account_state(app: &mut App) {
|
||||
if !app.has_screenshot() {
|
||||
return;
|
||||
@@ -118,7 +168,7 @@ pub(super) fn force_account_state(app: &mut App) {
|
||||
app.state.device_login_started =
|
||||
Some(std::time::Instant::now() - std::time::Duration::from_secs(3_600));
|
||||
}
|
||||
// 第三方站表单:直接把面板打开并预填地址(截图要看到输入框与校验按钮)
|
||||
// 第三方站表单:直接把面板打开并预填地址
|
||||
"authlib" | "authlib-form" => {
|
||||
app.account_form.panel = crate::pages::account::Panel::Authlib;
|
||||
if app.account_form.server.trim().is_empty() {
|
||||
@@ -127,13 +177,17 @@ pub(super) fn force_account_state(app: &mut App) {
|
||||
app.account_form.login = "steve@example.com".to_string();
|
||||
app.account_form.password = "not-a-real-password".to_string();
|
||||
}
|
||||
// 有账号时「Add an account」展开态(P2-4 的收敛后形态)
|
||||
"add" => {
|
||||
app.account_form.section_open = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::pages::account::{Message as Account, Panel, Track};
|
||||
use crate::pages::account::{Form, Panel, Track};
|
||||
|
||||
/// 表单态是纯 UI 状态:打开/关闭面板不派命令,但会把面板切对。
|
||||
///
|
||||
@@ -141,7 +195,6 @@ mod tests {
|
||||
/// 抓不住:真实 dispatch 的副作用(那要真后端)。
|
||||
#[test]
|
||||
fn the_form_state_follows_the_page_messages() {
|
||||
use crate::pages::account::Form;
|
||||
let mut form = Form::new();
|
||||
assert_eq!(form.panel, Panel::None);
|
||||
form.open(Track::Authlib);
|
||||
@@ -150,7 +203,14 @@ mod tests {
|
||||
form.close();
|
||||
assert_eq!(form.panel, Panel::None);
|
||||
assert!(form.password.is_empty(), "关闭面板必须把一次性密码清掉");
|
||||
}
|
||||
|
||||
let _ = Account::Open(Track::Offline);
|
||||
/// 三张轨道卡默认收起(有账号时),`ToggleAdd` 展开(复审 r1 P2-4)。
|
||||
#[test]
|
||||
fn the_add_section_starts_collapsed() {
|
||||
let mut form = Form::new();
|
||||
assert!(!form.section_open);
|
||||
form.section_open = true;
|
||||
assert!(form.section_open);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl App {
|
||||
self.refresh_view();
|
||||
}
|
||||
// M114:账户页(三轨登录 / 列表动作 / 表单输入)
|
||||
pages::Message::Account(message) => self.on_account(message),
|
||||
pages::Message::Account(message) => return self.on_account(message),
|
||||
// 还没接后端的占位动作:只打日志(哪几个、为什么,见 pages 模块文档的偏离登记)
|
||||
pages::Message::Action(action) => {
|
||||
eprintln!("[page] 占位动作 {action:?} pressed(没有对应 AppCommand)");
|
||||
|
||||
@@ -18,6 +18,7 @@ use super::message::{nav_item_of, Message};
|
||||
use super::state::{log_first_frame, App};
|
||||
use super::APP_TITLE;
|
||||
|
||||
mod account_page;
|
||||
mod logs;
|
||||
mod rail;
|
||||
|
||||
@@ -58,10 +59,12 @@ impl App {
|
||||
|
||||
let account = self.rail_account();
|
||||
match pages::Screen::of(self.state.route()) {
|
||||
Some(_) => shell::view_with_background(
|
||||
Some(_) => shell::view_with_account(
|
||||
&self.ui,
|
||||
nav_item_of(self.state.route()),
|
||||
&account,
|
||||
shell::NavRail {
|
||||
selected: nav_item_of(self.state.route()),
|
||||
account: account.clone(),
|
||||
},
|
||||
APP_TITLE,
|
||||
background,
|
||||
// M101 r2:设置页的只读状态(外观运行期状态 + 各组自己的状态)一次递进去。
|
||||
@@ -83,36 +86,17 @@ impl App {
|
||||
self.lang(),
|
||||
),
|
||||
// 账户页(M114):它不走 `pages::Screen` 那四页的 `Model`,自己一棵树
|
||||
// (模型在 `App::account`,由 `refresh_view` 按同一条门控重建;宽度用 M104 的
|
||||
// 中栏可用宽,与其它页同源)。
|
||||
None if self.state.route() == &Route::Accounts => shell::view_with_background(
|
||||
&self.ui,
|
||||
NavItem::Account,
|
||||
&account,
|
||||
APP_TITLE,
|
||||
background,
|
||||
pages::account::view(
|
||||
&self.ui,
|
||||
pages::layout::frame_metrics(
|
||||
pages::layout::PaneKit::Single,
|
||||
self.account.width,
|
||||
),
|
||||
&self.account,
|
||||
self.lang(),
|
||||
)
|
||||
.map(Message::Page),
|
||||
agent::docked(&self.ui, &self.agent, self.width)
|
||||
.map(|element| element.map(Message::Agent)),
|
||||
overlay,
|
||||
self.options.force_state,
|
||||
// M106:导航轨标签按当前界面语言取词。
|
||||
self.lang(),
|
||||
),
|
||||
// (树在 `app/view/account_page.rs`,与其它页共用 M104 的中栏可用宽)。
|
||||
None if self.state.route() == &Route::Accounts => {
|
||||
self.account_page(&account, background, overlay)
|
||||
}
|
||||
// `Route::Agent`:独立页与右栏共用同一个 `conversation::view`(`agent::page` 内部调的它)。
|
||||
None => shell::view_with_background(
|
||||
None => shell::view_with_account(
|
||||
&self.ui,
|
||||
NavItem::Ai,
|
||||
&account,
|
||||
shell::NavRail {
|
||||
selected: NavItem::Ai,
|
||||
account: account.clone(),
|
||||
},
|
||||
APP_TITLE,
|
||||
background,
|
||||
agent::page(&self.ui, &self.agent, self.width).map(Message::Agent),
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//! 账户页(M114)的**整页装配**:从 `App` 的状态拼出内容树,再交给外壳。
|
||||
//!
|
||||
//! 为什么单独一个文件:`app/view.rs` 贴着 400 行硬线(M104 的右栏分支已经把余量吃掉了),
|
||||
//! 而这段是纯装配(没有状态数学),搬出来不改变任何语义。
|
||||
//!
|
||||
//! 宽度口径:用 `AccountPage::width`(= M104 的 `AgentUi::content_width`,中栏可用宽),
|
||||
//! 与其它页、与 `refresh_view` 同一份来源——右栏展开时账户页**一起收缩**,不会顶到面板底下。
|
||||
|
||||
use iced::Element;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::pages;
|
||||
use crate::shell::{self, agent, NavItem, RailAccount};
|
||||
|
||||
use super::super::state::App;
|
||||
use super::super::APP_TITLE;
|
||||
|
||||
impl App {
|
||||
/// 账户页(走 `pages::account` 自己的树,而不是 `pages::Screen` 那四页的 `Model`)。
|
||||
pub(super) fn account_page<'a>(
|
||||
&'a self,
|
||||
account: &RailAccount,
|
||||
background: Option<Element<'a, Message>>,
|
||||
overlay: Option<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
let metrics =
|
||||
pages::layout::frame_metrics(pages::layout::PaneKit::Single, self.account.width);
|
||||
shell::view_with_account(
|
||||
&self.ui,
|
||||
shell::NavRail {
|
||||
selected: NavItem::Account,
|
||||
account: account.clone(),
|
||||
},
|
||||
APP_TITLE,
|
||||
background,
|
||||
// M106:账户页的文案跟着**生效语言**走(早期钉死 `Lang::En` 的写法已删)。
|
||||
pages::account::view(&self.ui, metrics, &self.account, self.lang()).map(Message::Page),
|
||||
agent::docked(&self.ui, &self.agent, self.width)
|
||||
.map(|element| element.map(Message::Agent)),
|
||||
overlay,
|
||||
self.options.force_state,
|
||||
self.lang(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user