同步 Android 启动页到 iOS,并在冷启动期间静默恢复登录态。
新增 SplashView、SplashCoordinator 与 Launch Screen 资源,移除冷启动 Lottie;LoginView 加载 App 配置。同时将 token 存储迁移至 UserDefaults,并更新相关测试与文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
26
suixinkan/Features/Auth/API/AppConfigAPI.swift
Normal file
26
suixinkan/Features/Auth/API/AppConfigAPI.swift
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// AppConfigAPI.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// App 远程配置 API,封装 `/api/app/config` 接口。
|
||||
final class AppConfigAPI {
|
||||
private let client: APIClient
|
||||
|
||||
/// 初始化 App 配置 API,并注入共享网络客户端。
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 获取 App 远程配置,例如是否开放注册入口。
|
||||
func fetchAppConfig() async throws -> AppConfigResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/app/config")
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -56,7 +56,7 @@ Auth 模块负责登录页、手机号密码登录、多账号选择和账号选
|
||||
## 缓存规则
|
||||
|
||||
Auth 模块不直接写缓存。登录成功后的缓存由 `AuthSessionCoordinator` 负责:
|
||||
- 正式 token 写 Keychain。
|
||||
- 正式 token 写 UserDefaults。
|
||||
- 上次手机号和协议状态写 UserDefaults。
|
||||
- 账号资料、当前角色和业务作用域写入账号快照。
|
||||
|
||||
|
||||
22
suixinkan/Features/Auth/Models/AppConfigModels.swift
Normal file
22
suixinkan/Features/Auth/Models/AppConfigModels.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// AppConfigModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// App 远程配置响应,对齐 Android `AppConfigResponse`。
|
||||
struct AppConfigResponse: Decodable, Equatable {
|
||||
let enableRegister: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case enableRegister = "enable_register"
|
||||
}
|
||||
|
||||
/// 创建 App 配置响应,默认关闭注册入口。
|
||||
init(enableRegister: Bool = false) {
|
||||
self.enableRegister = enableRegister
|
||||
}
|
||||
}
|
||||
@ -78,6 +78,7 @@ final class LoginViewModel: ObservableObject {
|
||||
@Published var showsPassword = false
|
||||
@Published var showsAgreementSheet = false
|
||||
@Published var pendingAccountSelection: AccountSelectionPayload?
|
||||
@Published private(set) var enableRegister = false
|
||||
|
||||
var canSubmit: Bool {
|
||||
isValidPhone && !trimmedPassword.isEmpty
|
||||
@ -124,6 +125,16 @@ final class LoginViewModel: ObservableObject {
|
||||
showsAgreementSheet = false
|
||||
}
|
||||
|
||||
/// 加载 App 远程配置;请求失败时保持默认值,与 Android 登录页行为一致。
|
||||
func loadAppConfig(appConfigAPI: AppConfigAPI) async {
|
||||
do {
|
||||
let config = try await appConfigAPI.fetchAppConfig()
|
||||
enableRegister = config.enableRegister
|
||||
} catch {
|
||||
// 请求失败不做任何提示,保持默认值 false。
|
||||
}
|
||||
}
|
||||
|
||||
/// 当输入包含 +86 前缀时,把手机号规范化为 11 位国内手机号。
|
||||
func normalizeUsernameCountryCodeIfNeeded() {
|
||||
let digits = username.filter(\.isNumber)
|
||||
|
||||
@ -14,6 +14,7 @@ struct LoginView: View {
|
||||
@EnvironmentObject private var permissionContext: PermissionContext
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.authAPI) private var authAPI
|
||||
@Environment(\.appConfigAPI) private var appConfigAPI
|
||||
@Environment(\.profileAPI) private var profileAPI
|
||||
@Environment(\.accountContextAPI) private var accountContextAPI
|
||||
@Environment(\.authSessionCoordinator) private var authSessionCoordinator
|
||||
@ -85,6 +86,7 @@ struct LoginView: View {
|
||||
}
|
||||
.task {
|
||||
viewModel.applyPreferences(authSessionCoordinator.loginPreferences())
|
||||
await viewModel.loadAppConfig(appConfigAPI: appConfigAPI)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
suixinkan/Features/Splash/Splash.md
Normal file
35
suixinkan/Features/Splash/Splash.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Splash 模块业务逻辑
|
||||
|
||||
## 模块职责
|
||||
|
||||
Splash 模块负责冷启动时的品牌启动页展示,对齐 Android `SplashActivity` + `activity_splash.xml` 的视觉规范。
|
||||
|
||||
该模块不处理登录、隐私协议或远程配置,只承担启动阶段的品牌展示和时序控制。
|
||||
|
||||
## 核心对象
|
||||
|
||||
- `SplashView`:展示白底、180pt Logo、「随心瞰 / 商家版」文案。
|
||||
- `SplashCoordinator`:并行执行最短 1.5 秒展示与 `SessionBootstrapper.restore`,两者都完成后结束 Splash。
|
||||
- `SplashLogo` / `LaunchBackground`:启动页图片与背景色资源,同时用于系统 `UILaunchScreen`。
|
||||
|
||||
## 展示规则
|
||||
|
||||
- 背景色:白色 `#FFFFFF`
|
||||
- Logo:`SplashLogo`,180×180 pt
|
||||
- 标题:`随心瞰`,40pt,bold + italic,`AppDesign.primary`
|
||||
- 副标题:`商家版`,20pt,bold,距底 32pt
|
||||
|
||||
## 启动时序
|
||||
|
||||
1. iOS 系统 Launch Screen 展示白底 + Logo。
|
||||
2. `RootView` 挂载后立即显示 `SplashView`。
|
||||
3. `SplashCoordinator.start` 并行执行:
|
||||
- 最短展示 1.5 秒(UI Test 模式跳过)
|
||||
- `SessionBootstrapper.restore` 静默恢复登录态
|
||||
4. 两者都完成后淡出 Splash,进入 `LoginView` 或 `MainTabsView`。
|
||||
5. 冷启动期间不显示 Lottie 全局 Loading。
|
||||
|
||||
## 与 Android 的差异
|
||||
|
||||
- Android 首启隐私弹窗在 Splash 上;iOS 仍在登录页勾选协议。
|
||||
- Android 系统 Launch Screen 与应用内 Splash 使用同一 XML;iOS 系统 Launch Screen 仅展示 Logo,文案由 SwiftUI Splash 补齐。
|
||||
47
suixinkan/Features/Splash/Views/SplashView.swift
Normal file
47
suixinkan/Features/Splash/Views/SplashView.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// SplashView.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 启动页视图,对齐 Android `activity_splash.xml` 的品牌展示布局。
|
||||
struct SplashView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.white
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
Image("SplashLogo")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 180, height: 180)
|
||||
.accessibilityIdentifier("splash.logo")
|
||||
|
||||
Text("随心瞰")
|
||||
.font(.system(size: 40, weight: .bold))
|
||||
.italic()
|
||||
.foregroundStyle(AppDesign.primary)
|
||||
.accessibilityIdentifier("splash.title")
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("商家版")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.foregroundStyle(AppDesign.primary)
|
||||
.padding(.bottom, 32)
|
||||
.accessibilityIdentifier("splash.subtitle")
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier("splash.root")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SplashView()
|
||||
}
|
||||
Reference in New Issue
Block a user