同步 Android 启动页到 iOS,并在冷启动期间静默恢复登录态。

新增 SplashView、SplashCoordinator 与 Launch Screen 资源,移除冷启动 Lottie;LoginView 加载 App 配置。同时将 token 存储迁移至 UserDefaults,并更新相关测试与文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 14:59:44 +08:00
parent 08492df6ce
commit 5bdf4a7dbf
29 changed files with 501 additions and 91 deletions

View 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")
)
}
}

View File

@ -56,7 +56,7 @@ Auth 模块负责登录页、手机号密码登录、多账号选择和账号选
## 缓存规则
Auth 模块不直接写缓存。登录成功后的缓存由 `AuthSessionCoordinator` 负责:
- 正式 token 写 Keychain
- 正式 token 写 UserDefaults
- 上次手机号和协议状态写 UserDefaults。
- 账号资料、当前角色和业务作用域写入账号快照。

View 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
}
}

View File

@ -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)

View File

@ -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)
}
}

View 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
- 标题:`随心瞰`40ptbold + italic`AppDesign.primary`
- 副标题:`商家版`20ptbold距底 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 使用同一 XMLiOS 系统 Launch Screen 仅展示 Logo文案由 SwiftUI Splash 补齐。

View 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()
}