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,126 @@
|
||||
//! 截图/演示用的账户矩阵(**只在 mock 后端**里被调用)。
|
||||
//!
|
||||
//! D30 要求账户页出五态 × 浅深:空态 / 多账号列表 / 设备码进行中 / 登录失败 / 第三方站表单。
|
||||
//! 截图通道强制 mock 后端(`AppOptions::mock`),所以这五态的**数据**必须由 mock 造:
|
||||
//! 走 `ULL_ACCOUNT_STATE` 选场景,与 `ULL_START_ROUTE` / `ULL_APPEARANCE_*` 同一套取证开关。
|
||||
//!
|
||||
//! ## 造的是什么、不造什么
|
||||
//!
|
||||
//! - 列表行造的是**公开字段**(名字 / 轨道 / 创建时间 / 服务器),与真 host 的
|
||||
//! `AccountView` 同构;
|
||||
//! - **不造皮肤字节**:第三方轨的真图来自服务器,mock 里没有(伪造一张「假皮肤」正好违反
|
||||
//! M114 的口径)。所以截图里皮肤区显示的是首字母块 + 「没有可用皮肤」。
|
||||
|
||||
use crate::dto::{AccountKind, AccountView, DeviceLoginView};
|
||||
|
||||
/// 场景名(`ULL_ACCOUNT_STATE`;未知值按 `list` 处理)。
|
||||
pub(super) fn scenario() -> String {
|
||||
std::env::var("ULL_ACCOUNT_STATE")
|
||||
.unwrap_or_else(|_| "list".to_string())
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
}
|
||||
|
||||
/// 场景对应的账户列表(`empty` 场景是空表 ⇒ 空态)。
|
||||
pub(super) fn accounts(scenario: &str) -> Vec<AccountView> {
|
||||
if scenario == "empty" {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
account(
|
||||
"account:mock-msa",
|
||||
"Steve",
|
||||
AccountKind::Microsoft,
|
||||
"8f2f1d3c4b5a69788796a5b4c3d2e1f0",
|
||||
"steve@outlook.com",
|
||||
"2026-10-28 20:12",
|
||||
true,
|
||||
"",
|
||||
),
|
||||
account(
|
||||
"account:mock-authlib",
|
||||
"Alex",
|
||||
AccountKind::Authlib,
|
||||
"0a1b2c3d4e5f60718293a4b5c6d7e8f9",
|
||||
"alex@example.com",
|
||||
"2026-10-30 09:40",
|
||||
false,
|
||||
"https://littleskin.cn/api/yggdrasil/",
|
||||
),
|
||||
account(
|
||||
"account:mock-offline",
|
||||
"Herobrine",
|
||||
AccountKind::Offline,
|
||||
"3c4d5e6f708192a3b4c5d6e7f8091a2b",
|
||||
"",
|
||||
"2026-10-31 21:05",
|
||||
false,
|
||||
"",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// 场景对应的登录面板状态(`None` = 没有面板)。
|
||||
pub(super) fn device_login(scenario: &str) -> Option<DeviceLoginView> {
|
||||
match scenario {
|
||||
"device" | "expired" => Some(DeviceLoginView::waiting(
|
||||
"ABCD-EFGH",
|
||||
"https://www.microsoft.com/link",
|
||||
900,
|
||||
"",
|
||||
)),
|
||||
"failed" => Some(DeviceLoginView::failed(
|
||||
"微软拒绝了这次登录:AADSTS7000218(HTTP 403)—— 应用白名单还没批准,或网络被拦截",
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn account(
|
||||
id: &str,
|
||||
name: &str,
|
||||
kind: AccountKind,
|
||||
profile_id: &str,
|
||||
login_name: &str,
|
||||
created: &str,
|
||||
active: bool,
|
||||
server_url: &str,
|
||||
) -> AccountView {
|
||||
AccountView {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
kind,
|
||||
profile_id: profile_id.to_string(),
|
||||
login_name: login_name.to_string(),
|
||||
created: created.to_string(),
|
||||
active,
|
||||
server_url: server_url.to_string(),
|
||||
skin: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_empty_scenario_has_no_rows_and_the_list_scenario_covers_all_three_tracks() {
|
||||
assert!(accounts("empty").is_empty());
|
||||
let rows = accounts("list");
|
||||
assert_eq!(rows.len(), 3);
|
||||
assert!(rows.iter().any(|row| row.kind == AccountKind::Microsoft));
|
||||
assert!(rows.iter().any(|row| row.kind == AccountKind::Authlib));
|
||||
assert!(rows.iter().any(|row| row.kind == AccountKind::Offline));
|
||||
assert_eq!(rows.iter().filter(|row| row.active).count(), 1);
|
||||
assert!(rows.iter().all(|row| row.skin.is_none()), "mock 不伪造皮肤");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_device_states_are_renderable() {
|
||||
assert!(device_login("device").is_some_and(|view| !view.user_code.is_empty()));
|
||||
assert!(device_login("failed")
|
||||
.is_some_and(|view| view.phase == crate::dto::DeviceLoginPhase::Failed));
|
||||
assert!(device_login("list").is_none());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user