feat(app-iced): M114 账户页三轨登录(微软正版 / 第三方皮肤站 / 本地档案)
- 账户页:当前账户卡 + 全部账户列表 + 空态三入口 + 皮肤区;五项动作(添加/切换/删除/重新登录/校验) - 三轨添加流程:设备码(用户码+链接+倒计时+过期重试+取消)、授权码(浏览器+可复制链接+取消)、 本地档案(名字校验)、第三方站(服务地址 + 服务发现校验 + 密码登录,密码掩码且用完即弃) - 导航:账户为一等页,入口是轨底头像位(未登录=描边剪影 / 已登录=首字母块+名字;窄高收成图标条) - 凭据红线:DTO 侧形状脱敏 + 状态层二次脱敏 + 密码掩码;Inject-JWT 测试断言视图模型干净(已做变异取证) - presenter:authlib 登录/服务发现/皮肤抓取(复用下载引擎的 HTTP 客户端,无新增依赖)、 皮肤原图经 DTO 到 UI 裁剪成头像(既有 image 依赖,未改 Cargo.toml) - i18n:新增 account 域 51 条中英文案(离线档案口径全量写入),金表同步
This commit is contained in:
1 parent
e7b04c4afe
commit
1ff00bbe50
43 files changed
+4343
-117
No files matched your search
@@ -0,0 +1,149 @@
|
||||
//! 账户页(M114):**三轨**登录(微软正版 / 第三方皮肤站 / 本地档案)+ 账户列表 + 皮肤区。
|
||||
//!
|
||||
//! ## 这一页回答什么
|
||||
//!
|
||||
//! 用户的原话是「用户管理页面还没做吗?包括不限于正版登录 离线登录 第三方皮肤站登录」。
|
||||
//! Slint 版曾有这个页面(`DevDocs/ops/screens-m43/account-data.png`),换 iced 时没搬过来,
|
||||
//! 于是 iced 版**一个账户入口都没有**——本页就是把它补回来,并按 M114 的口径做成三轨。
|
||||
//!
|
||||
//! ## 四条红线(都在本页的断言里)
|
||||
//!
|
||||
//! | 红线 | 落点 |
|
||||
//! |---|---|
|
||||
//! | 页面不得出现任何 token / refresh_token | 上屏文案只来自 [`model`] 的公开字段;`tests` 里有「把 JWT 塞进事件、断言模型干净」的用例 |
|
||||
//! | 离线档案的说明必须带「仅本地/隔离网络测试用,无法连接启用正版验证的服务器」 | `Key::AccountTrackOfflineBody` / `Key::AccountOfflineWarning` 两处都写全 |
|
||||
//! | 设备码不得只转圈 | [`login`]:用户码 + 链接 + 倒计时 + 「已过期,重试」 |
|
||||
//! | 没有皮肤不给假皮肤 | 头像位 = 首字母块(`profile_id` 稳定取色);第三方轨才可能出真图 |
|
||||
//!
|
||||
//! ## 模块
|
||||
//!
|
||||
//! | 文件 | 职责 |
|
||||
//! |---|---|
|
||||
//! | [`model`] | 状态 → 视图模型(列表行 / 当前账户 / 三条轨道的面板态) |
|
||||
//! | [`avatar`] | 首字母块 + 皮肤图(解码/裁剪缓存;失败一律退回首字母块,**绝不白块**) |
|
||||
//! | [`widgets`] | 本页的小件(头像块 / 章节标题 / 徽标),样式全部走令牌 |
|
||||
//! | [`rows`] | 当前账户卡片 + 账户列表行 |
|
||||
//! | [`forms`] | 三条轨道的入口卡 + 本地档案/第三方站表单 |
|
||||
//! | [`login`] | 微软登录面板(设备码倒计时 / 浏览器授权 / 失败态) |
|
||||
//! | `tests` | 视图模型的判据(凭据红线、空态、倒计时、皮肤回落) |
|
||||
|
||||
pub mod avatar;
|
||||
pub mod forms;
|
||||
pub mod login;
|
||||
pub mod model;
|
||||
pub mod panels;
|
||||
pub mod rows;
|
||||
pub mod widgets;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use model::{Form, Page, Panel, Track};
|
||||
|
||||
use iced::widget::{column, scrollable};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_i18n::{Key, Lang};
|
||||
|
||||
use crate::app::components::buttons::{self, Button};
|
||||
use crate::pages::frame::{self, Header};
|
||||
use crate::pages::layout::FrameMetrics;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::Message as PageMessage;
|
||||
|
||||
/// 账户页的页面消息(app 层把它翻成 `AppCommand` / 状态写入)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Message {
|
||||
/// 打开某条轨道的表单
|
||||
Open(Track),
|
||||
/// 关掉表单/登录面板
|
||||
Close,
|
||||
/// 第三方站地址输入
|
||||
Server(String),
|
||||
/// 登录名输入
|
||||
Login(String),
|
||||
/// 密码输入(只存在内存里,随请求交给 host,用完即弃)
|
||||
Password(String),
|
||||
/// 本地档案名输入
|
||||
OfflineName(String),
|
||||
/// 校验服务器地址(服务发现)
|
||||
Probe,
|
||||
/// 表单提交(本地档案 → 建档案;第三方站 → 登录)
|
||||
Submit,
|
||||
/// 微软:设备码流
|
||||
StartDevice,
|
||||
/// 微软:授权码流(拉起系统浏览器)
|
||||
StartBrowser,
|
||||
/// 取消/关掉登录面板
|
||||
Cancel,
|
||||
/// 复制一段文本(用户码 / 链接)
|
||||
Copy(String),
|
||||
/// 切换当前启动账户
|
||||
Switch(String),
|
||||
/// 删除账户
|
||||
Remove(String),
|
||||
/// 重新登录(微软轨 → 授权码流)
|
||||
Relogin,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// 包成页面的消息(本模块内部与 app 层都用它,保证只有一处包装点)。
|
||||
pub fn into_page(self) -> PageMessage {
|
||||
PageMessage::Account(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// 整页。
|
||||
pub fn view<'a>(
|
||||
ui: &'a Ui,
|
||||
metrics: FrameMetrics,
|
||||
page: &'a Page,
|
||||
lang: Lang,
|
||||
) -> Element<'a, PageMessage> {
|
||||
let text = lang.catalog();
|
||||
let subtitle = text.text(Key::AccountSubtitle);
|
||||
let add = text.text(Key::AccountEmptyAction);
|
||||
let header = Header::new(text.text(Key::AccountTitle))
|
||||
.subtitle(subtitle)
|
||||
.action(buttons::tonal(
|
||||
ui,
|
||||
Button::new(add, Message::Open(Track::Microsoft).into_page()),
|
||||
));
|
||||
|
||||
let top = if page.rows.is_empty() {
|
||||
forms::empty(ui, text, lang)
|
||||
} else {
|
||||
column![
|
||||
rows::active_card(ui, page, text, lang),
|
||||
rows::list(ui, page, text, lang),
|
||||
]
|
||||
.spacing(ui.space(Space::X6))
|
||||
.into()
|
||||
};
|
||||
|
||||
// 三条轨道入口 + 表单/登录面板:面板存在时**只画面板**,且**置顶**——
|
||||
// 否则在 1180×760 下设备码的「复制用户码 / 取消」会掉到折线以下(D30 复审实测)。
|
||||
let flows = match page.panel {
|
||||
Panel::None => forms::tracks(ui, text, lang),
|
||||
_ => forms::panel(ui, page, text, lang),
|
||||
};
|
||||
|
||||
let mut content = column![].spacing(ui.space(Space::X6)).width(Length::Fill);
|
||||
content = if page.panel == Panel::None {
|
||||
content.push(top).push(flows)
|
||||
} else {
|
||||
content.push(flows).push(top)
|
||||
};
|
||||
|
||||
frame::page(
|
||||
ui,
|
||||
metrics,
|
||||
header,
|
||||
scrollable(content)
|
||||
.style(frame::scroll_style(ui))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user