diff --git a/suixinkan.xcodeproj/project.pbxproj b/suixinkan.xcodeproj/project.pbxproj
index 6a8aa08..25d5189 100644
--- a/suixinkan.xcodeproj/project.pbxproj
+++ b/suixinkan.xcodeproj/project.pbxproj
@@ -163,7 +163,6 @@
INFOPLIST_FILE = suixinkan/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
- INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16;
@@ -200,7 +199,6 @@
INFOPLIST_FILE = suixinkan/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
- INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16;
diff --git a/suixinkan/App/AppRouter.swift b/suixinkan/App/AppRouter.swift
new file mode 100644
index 0000000..c7adff6
--- /dev/null
+++ b/suixinkan/App/AppRouter.swift
@@ -0,0 +1,57 @@
+//
+// AppRouter.swift
+// suixinkan
+//
+
+import UIKit
+
+/// 应用根页面路由。
+enum AppRouter {
+
+ enum Root {
+ case login
+ case mainTab
+ }
+
+ /// 根据登录态创建根控制器:已登录 → 主页 Tab,未登录 → 登录页。
+ static func makeRootViewController() -> UIViewController {
+ makeViewController(for: AppStore.shared.isLoggedIn ? .mainTab : .login)
+ }
+
+ /// 切换窗口根控制器。
+ static func setRoot(_ root: Root, on window: UIWindow?, animated: Bool = true) {
+ setRoot(makeViewController(for: root), on: window, animated: animated)
+ }
+
+ /// 根据当前登录态刷新根控制器。
+ static func refreshRoot(on window: UIWindow?, animated: Bool = true) {
+ setRoot(AppStore.shared.isLoggedIn ? .mainTab : .login, on: window, animated: animated)
+ }
+
+ private static func makeViewController(for root: Root) -> UIViewController {
+ switch root {
+ case .login:
+ return UINavigationController(rootViewController: LoginViewController())
+ case .mainTab:
+ return MainTabBarController()
+ }
+ }
+
+ private static func setRoot(_ viewController: UIViewController, on window: UIWindow?, animated: Bool) {
+ guard let window else { return }
+
+ guard animated, let snapshot = window.snapshotView(afterScreenUpdates: true) else {
+ window.rootViewController = viewController
+ return
+ }
+
+ viewController.view.addSubview(snapshot)
+ window.rootViewController = viewController
+
+ UIView.animate(withDuration: 0.25, animations: {
+ snapshot.alpha = 0
+ }, completion: { _ in
+ snapshot.removeFromSuperview()
+ })
+ }
+}
diff --git a/suixinkan/Assets.xcassets/icon_cb_privacy_selected.imageset/Contents.json b/suixinkan/Assets.xcassets/icon_cb_privacy_selected.imageset/Contents.json
new file mode 100644
index 0000000..7c9a98d
--- /dev/null
+++ b/suixinkan/Assets.xcassets/icon_cb_privacy_selected.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_cb_privacy_selected.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/icon_cb_privacy_selected.imageset/icon_cb_privacy_selected.png b/suixinkan/Assets.xcassets/icon_cb_privacy_selected.imageset/icon_cb_privacy_selected.png
new file mode 100644
index 0000000..7dfce28
Binary files /dev/null and b/suixinkan/Assets.xcassets/icon_cb_privacy_selected.imageset/icon_cb_privacy_selected.png differ
diff --git a/suixinkan/Assets.xcassets/icon_cb_privacy_unselect.imageset/Contents.json b/suixinkan/Assets.xcassets/icon_cb_privacy_unselect.imageset/Contents.json
new file mode 100644
index 0000000..c688206
--- /dev/null
+++ b/suixinkan/Assets.xcassets/icon_cb_privacy_unselect.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_cb_privacy_unselect.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/icon_cb_privacy_unselect.imageset/icon_cb_privacy_unselect.png b/suixinkan/Assets.xcassets/icon_cb_privacy_unselect.imageset/icon_cb_privacy_unselect.png
new file mode 100644
index 0000000..c53d8fd
Binary files /dev/null and b/suixinkan/Assets.xcassets/icon_cb_privacy_unselect.imageset/icon_cb_privacy_unselect.png differ
diff --git a/suixinkan/Assets.xcassets/icon_pwd_invisible.imageset/Contents.json b/suixinkan/Assets.xcassets/icon_pwd_invisible.imageset/Contents.json
new file mode 100644
index 0000000..231bbbb
--- /dev/null
+++ b/suixinkan/Assets.xcassets/icon_pwd_invisible.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_pwd_invisible.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/icon_pwd_invisible.imageset/icon_pwd_invisible.png b/suixinkan/Assets.xcassets/icon_pwd_invisible.imageset/icon_pwd_invisible.png
new file mode 100644
index 0000000..fd2ac79
Binary files /dev/null and b/suixinkan/Assets.xcassets/icon_pwd_invisible.imageset/icon_pwd_invisible.png differ
diff --git a/suixinkan/Assets.xcassets/icon_pwd_visible.imageset/Contents.json b/suixinkan/Assets.xcassets/icon_pwd_visible.imageset/Contents.json
new file mode 100644
index 0000000..4d9eeae
--- /dev/null
+++ b/suixinkan/Assets.xcassets/icon_pwd_visible.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_pwd_visible.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/icon_pwd_visible.imageset/icon_pwd_visible.png b/suixinkan/Assets.xcassets/icon_pwd_visible.imageset/icon_pwd_visible.png
new file mode 100644
index 0000000..df11b0c
Binary files /dev/null and b/suixinkan/Assets.xcassets/icon_pwd_visible.imageset/icon_pwd_visible.png differ
diff --git a/suixinkan/Assets.xcassets/icon_scan.imageset/Contents.json b/suixinkan/Assets.xcassets/icon_scan.imageset/Contents.json
new file mode 100644
index 0000000..c5d9acc
--- /dev/null
+++ b/suixinkan/Assets.xcassets/icon_scan.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_scan.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "icon_scan@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "icon_scan@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan.png b/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan.png
new file mode 100644
index 0000000..a03cfd1
Binary files /dev/null and b/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan.png differ
diff --git a/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan@2x.png b/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan@2x.png
new file mode 100644
index 0000000..1f76573
Binary files /dev/null and b/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan@2x.png differ
diff --git a/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan@3x.png b/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan@3x.png
new file mode 100644
index 0000000..72e3340
Binary files /dev/null and b/suixinkan/Assets.xcassets/icon_scan.imageset/icon_scan@3x.png differ
diff --git a/suixinkan/Assets.xcassets/img_login_bg.imageset/Contents.json b/suixinkan/Assets.xcassets/img_login_bg.imageset/Contents.json
new file mode 100644
index 0000000..1a33e0a
--- /dev/null
+++ b/suixinkan/Assets.xcassets/img_login_bg.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "img_login_bg.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/img_login_bg.imageset/img_login_bg.png b/suixinkan/Assets.xcassets/img_login_bg.imageset/img_login_bg.png
new file mode 100644
index 0000000..163b509
Binary files /dev/null and b/suixinkan/Assets.xcassets/img_login_bg.imageset/img_login_bg.png differ
diff --git a/suixinkan/Assets.xcassets/tab_data_selected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_data_selected.imageset/Contents.json
new file mode 100644
index 0000000..90c8ea2
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_data_selected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_data_selected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_data_selected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_data_selected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected.png b/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected.png
new file mode 100644
index 0000000..6a5fe1c
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected@2x.png b/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected@2x.png
new file mode 100644
index 0000000..84e2fb9
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected@3x.png b/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected@3x.png
new file mode 100644
index 0000000..50a0640
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_data_selected.imageset/tab_data_selected@3x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_data_unselected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/Contents.json
new file mode 100644
index 0000000..254b4fb
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_data_unselected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_data_unselected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_data_unselected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected.png b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected.png
new file mode 100644
index 0000000..cd2a559
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected@2x.png b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected@2x.png
new file mode 100644
index 0000000..c1031e8
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected@3x.png b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected@3x.png
new file mode 100644
index 0000000..4e875e1
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_data_unselected.imageset/tab_data_unselected@3x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_home_selected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_home_selected.imageset/Contents.json
new file mode 100644
index 0000000..566e1d5
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_home_selected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_home_selected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_home_selected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_home_selected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected.png b/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected.png
new file mode 100644
index 0000000..f59bbb8
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected@2x.png b/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected@2x.png
new file mode 100644
index 0000000..50f856b
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected@3x.png b/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected@3x.png
new file mode 100644
index 0000000..f7c4f2c
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_home_selected.imageset/tab_home_selected@3x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_home_unselected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/Contents.json
new file mode 100644
index 0000000..e6194ed
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_home_unselected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_home_unselected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_home_unselected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected.png b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected.png
new file mode 100644
index 0000000..1cdaf5c
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected@2x.png b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected@2x.png
new file mode 100644
index 0000000..54fb11a
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected@3x.png b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected@3x.png
new file mode 100644
index 0000000..95bc97d
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_home_unselected.imageset/tab_home_unselected@3x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_order_selected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_order_selected.imageset/Contents.json
new file mode 100644
index 0000000..1e81253
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_order_selected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_order_selected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_order_selected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_order_selected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected.png b/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected.png
new file mode 100644
index 0000000..3627ef9
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected@2x.png b/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected@2x.png
new file mode 100644
index 0000000..9a18927
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected@3x.png b/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected@3x.png
new file mode 100644
index 0000000..255ebef
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_order_selected.imageset/tab_order_selected@3x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_order_unselected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/Contents.json
new file mode 100644
index 0000000..89b0894
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_order_unselected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_order_unselected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_order_unselected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected.png b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected.png
new file mode 100644
index 0000000..90a3d32
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected@2x.png b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected@2x.png
new file mode 100644
index 0000000..44ceb95
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected@3x.png b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected@3x.png
new file mode 100644
index 0000000..d49583c
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_order_unselected.imageset/tab_order_unselected@3x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_profile_selected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/Contents.json
new file mode 100644
index 0000000..33d4fa6
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_profile_selected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_profile_selected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_profile_selected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected.png b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected.png
new file mode 100644
index 0000000..c5f7477
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected@2x.png b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected@2x.png
new file mode 100644
index 0000000..e9ef614
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected@3x.png b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected@3x.png
new file mode 100644
index 0000000..b2996b3
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_profile_selected.imageset/tab_profile_selected@3x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/Contents.json b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/Contents.json
new file mode 100644
index 0000000..961a1dd
--- /dev/null
+++ b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "tab_profile_unselected.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "tab_profile_unselected@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "tab_profile_unselected@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected.png b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected.png
new file mode 100644
index 0000000..73f36da
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected.png differ
diff --git a/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected@2x.png b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected@2x.png
new file mode 100644
index 0000000..18591e7
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected@2x.png differ
diff --git a/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected@3x.png b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected@3x.png
new file mode 100644
index 0000000..d5abb60
Binary files /dev/null and b/suixinkan/Assets.xcassets/tab_profile_unselected.imageset/tab_profile_unselected@3x.png differ
diff --git a/suixinkan/DataStore/AppStore.swift b/suixinkan/DataStore/AppStore.swift
new file mode 100644
index 0000000..8cb1b70
--- /dev/null
+++ b/suixinkan/DataStore/AppStore.swift
@@ -0,0 +1,44 @@
+//
+// AppStore.swift
+// suixinkan
+//
+
+import Foundation
+
+/// 本地会话存储,与 Android `AppStoreDataSource` 对齐。
+/// 当前以 Token 是否非空判断登录态。
+final class AppStore {
+
+ static let shared = AppStore()
+
+ private enum Key {
+ static let token = "key_in_token"
+ }
+
+ private let defaults: UserDefaults
+
+ init(defaults: UserDefaults = .standard) {
+ self.defaults = defaults
+ }
+
+ /// 登录 Token。
+ var token: String {
+ get { defaults.string(forKey: Key.token) ?? "" }
+ set { defaults.set(newValue, forKey: Key.token) }
+ }
+
+ /// Token 非空视为已登录。
+ var isLoggedIn: Bool {
+ !token.isEmpty
+ }
+
+ /// 保存登录 Token。
+ func saveToken(_ token: String) {
+ self.token = token
+ }
+
+ /// 清除登录态。
+ func logout() {
+ token = ""
+ }
+}
diff --git a/suixinkan/Info.plist b/suixinkan/Info.plist
index dd3c9af..0eb786d 100644
--- a/suixinkan/Info.plist
+++ b/suixinkan/Info.plist
@@ -15,8 +15,6 @@
Default Configuration
UISceneDelegateClassName
$(PRODUCT_MODULE_NAME).SceneDelegate
- UISceneStoryboardFile
- Main
diff --git a/suixinkan/SceneDelegate.swift b/suixinkan/SceneDelegate.swift
index 2f9609f..8b0a23b 100644
--- a/suixinkan/SceneDelegate.swift
+++ b/suixinkan/SceneDelegate.swift
@@ -2,8 +2,6 @@
// SceneDelegate.swift
// suixinkan
//
-// Created by hanqiu on 2026/7/6.
-//
import UIKit
@@ -11,42 +9,57 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
+ func scene(
+ _ scene: UIScene,
+ willConnectTo session: UISceneSession,
+ options connectionOptions: UIScene.ConnectionOptions
+ ) {
+ guard let windowScene = scene as? UIWindowScene else { return }
- func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
- // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
- // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
- // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
- guard let _ = (scene as? UIWindowScene) else { return }
+ let window = UIWindow(windowScene: windowScene)
+ window.rootViewController = AppRouter.makeRootViewController()
+ window.makeKeyAndVisible()
+ self.window = window
+
+ registerNotifications()
}
func sceneDidDisconnect(_ scene: UIScene) {
- // Called as the scene is being released by the system.
- // This occurs shortly after the scene enters the background, or when its session is discarded.
- // Release any resources associated with this scene that can be re-created the next time the scene connects.
- // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
+ NotificationCenter.default.removeObserver(self)
}
- func sceneDidBecomeActive(_ scene: UIScene) {
- // Called when the scene has moved from an inactive state to an active state.
- // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
+ private func registerNotifications() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handleSessionDidExpire),
+ name: NotificationName.sessionDidExpire,
+ object: nil
+ )
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handleUserDidLogout),
+ name: NotificationName.userDidLogout,
+ object: nil
+ )
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handleUserDidLogin),
+ name: NotificationName.userDidLogin,
+ object: nil
+ )
}
- func sceneWillResignActive(_ scene: UIScene) {
- // Called when the scene will move from an active state to an inactive state.
- // This may occur due to temporary interruptions (ex. an incoming phone call).
+ @objc private func handleSessionDidExpire() {
+ AppStore.shared.logout()
+ AppRouter.setRoot(.login, on: window)
}
- func sceneWillEnterForeground(_ scene: UIScene) {
- // Called as the scene transitions from the background to the foreground.
- // Use this method to undo the changes made on entering the background.
+ @objc private func handleUserDidLogout() {
+ AppStore.shared.logout()
+ AppRouter.setRoot(.login, on: window)
}
- func sceneDidEnterBackground(_ scene: UIScene) {
- // Called as the scene transitions from the foreground to the background.
- // Use this method to save data, release shared resources, and store enough scene-specific state information
- // to restore the scene back to its current state.
+ @objc private func handleUserDidLogin() {
+ AppRouter.setRoot(.mainTab, on: window)
}
-
-
}
-
diff --git a/suixinkan/Theme/AppColor.swift b/suixinkan/Theme/AppColor.swift
new file mode 100644
index 0000000..9e7de15
--- /dev/null
+++ b/suixinkan/Theme/AppColor.swift
@@ -0,0 +1,19 @@
+//
+// AppColor.swift
+// suixinkan
+//
+
+import UIKit
+
+/// 应用主题色,与 Android `theme/Color.kt` 对齐。
+enum AppColor {
+
+ static let primary = UIColor(red: 0 / 255, green: 115 / 255, blue: 255 / 255, alpha: 1)
+ static let text333 = UIColor(red: 51 / 255, green: 51 / 255, blue: 51 / 255, alpha: 1)
+ static let text666 = UIColor(red: 102 / 255, green: 102 / 255, blue: 102 / 255, alpha: 1)
+ static let agreementLink = UIColor(red: 32 / 255, green: 139 / 255, blue: 255 / 255, alpha: 1)
+ static let inputBackground = UIColor(red: 244 / 255, green: 244 / 255, blue: 244 / 255, alpha: 1)
+ static let secondaryButtonBackground = UIColor(red: 239 / 255, green: 246 / 255, blue: 255 / 255, alpha: 1)
+ static let buttonDisabled = UIColor(red: 192 / 255, green: 192 / 255, blue: 192 / 255, alpha: 1)
+ static let placeholder = UIColor.gray
+}
diff --git a/suixinkan/Theme/UIColor+Hex.swift b/suixinkan/Theme/UIColor+Hex.swift
new file mode 100644
index 0000000..a0e26bd
--- /dev/null
+++ b/suixinkan/Theme/UIColor+Hex.swift
@@ -0,0 +1,19 @@
+//
+// UIColor+Hex.swift
+// suixinkan
+//
+
+import UIKit
+
+extension UIColor {
+
+ /// 通过 0xRRGGBB 和透明度创建 UIColor。
+ convenience init(hex: UInt, alpha: CGFloat = 1.0) {
+ self.init(
+ red: CGFloat((hex >> 16) & 0xff) / 255.0,
+ green: CGFloat((hex >> 8) & 0xff) / 255.0,
+ blue: CGFloat(hex & 0xff) / 255.0,
+ alpha: alpha
+ )
+ }
+}
diff --git a/suixinkan/UI/Common/AgreementRowView.swift b/suixinkan/UI/Common/AgreementRowView.swift
new file mode 100644
index 0000000..d8572d9
--- /dev/null
+++ b/suixinkan/UI/Common/AgreementRowView.swift
@@ -0,0 +1,114 @@
+//
+// AgreementRowView.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 协议勾选行,对应 Android `AgreementRow`。
+final class AgreementRowView: UIView, UITextViewDelegate {
+
+ private let checkButton = UIButton(type: .custom)
+ private let textView = UITextView()
+
+ var isChecked = false {
+ didSet { updateCheckboxImage() }
+ }
+
+ var onCheckedChange: (() -> Void)?
+ var onUserAgreementTap: (() -> Void)?
+ var onPrivacyPolicyTap: (() -> Void)?
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupUI()
+ setupConstraints()
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func setupUI() {
+ checkButton.addTarget(self, action: #selector(checkButtonTapped), for: .touchUpInside)
+ updateCheckboxImage()
+
+ textView.backgroundColor = .clear
+ textView.isEditable = false
+ textView.isScrollEnabled = false
+ textView.textContainerInset = .zero
+ textView.textContainer.lineFragmentPadding = 0
+ textView.delegate = self
+ textView.linkTextAttributes = [
+ .foregroundColor: AppColor.agreementLink,
+ .font: UIFont.systemFont(ofSize: 12, weight: .semibold),
+ ]
+ textView.attributedText = makeAgreementText()
+
+ addSubview(checkButton)
+ addSubview(textView)
+ }
+
+ private func setupConstraints() {
+ checkButton.snp.makeConstraints { make in
+ make.leading.top.equalToSuperview()
+ make.size.equalTo(18)
+ }
+
+ textView.snp.makeConstraints { make in
+ make.leading.equalTo(checkButton.snp.trailing).offset(8)
+ make.trailing.top.bottom.equalToSuperview()
+ }
+ }
+
+ private func updateCheckboxImage() {
+ let imageName = isChecked ? "icon_cb_privacy_selected" : "icon_cb_privacy_unselect"
+ checkButton.setImage(UIImage(named: imageName), for: .normal)
+ }
+
+ private func makeAgreementText() -> NSAttributedString {
+ let result = NSMutableAttributedString()
+ let normalAttributes: [NSAttributedString.Key: Any] = [
+ .font: UIFont.systemFont(ofSize: 12),
+ .foregroundColor: AppColor.text666,
+ ]
+ let linkFont = UIFont.systemFont(ofSize: 12, weight: .semibold)
+
+ result.append(NSAttributedString(string: "已阅读并同意", attributes: normalAttributes))
+ result.append(NSAttributedString(string: "《用户协议》", attributes: [
+ .font: linkFont,
+ .foregroundColor: AppColor.agreementLink,
+ .link: "tos",
+ ]))
+ result.append(NSAttributedString(string: "与", attributes: normalAttributes))
+ result.append(NSAttributedString(string: "《隐私政策》", attributes: [
+ .font: linkFont,
+ .foregroundColor: AppColor.agreementLink,
+ .link: "privacy",
+ ]))
+ return result
+ }
+
+ @objc private func checkButtonTapped() {
+ onCheckedChange?()
+ }
+
+ func textView(
+ _ textView: UITextView,
+ shouldInteractWith URL: URL,
+ in characterRange: NSRange,
+ interaction: UITextItemInteraction
+ ) -> Bool {
+ switch URL.absoluteString {
+ case "tos":
+ onUserAgreementTap?()
+ case "privacy":
+ onPrivacyPolicyTap?()
+ default:
+ break
+ }
+ return false
+ }
+}
diff --git a/suixinkan/UI/Common/AppButton.swift b/suixinkan/UI/Common/AppButton.swift
new file mode 100644
index 0000000..f70af10
--- /dev/null
+++ b/suixinkan/UI/Common/AppButton.swift
@@ -0,0 +1,59 @@
+//
+// AppButton.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 主按钮,对应 Android `AppButton`。
+final class AppButton: UIButton {
+
+ enum Style {
+ case primary
+ case secondary
+ }
+
+ private let style: Style
+
+ init(title: String, style: Style = .primary) {
+ self.style = style
+ super.init(frame: .zero)
+ setupUI(title: title)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override var isEnabled: Bool {
+ didSet { updateAppearance() }
+ }
+
+ private func setupUI(title: String) {
+ titleLabel?.font = .systemFont(ofSize: 14, weight: .regular)
+ layer.cornerRadius = 10
+ clipsToBounds = true
+ setTitle(title, for: .normal)
+ updateAppearance()
+
+ snp.makeConstraints { make in
+ make.height.equalTo(46)
+ }
+ }
+
+ private func updateAppearance() {
+ switch style {
+ case .primary:
+ backgroundColor = isEnabled ? AppColor.primary : AppColor.buttonDisabled
+ setTitleColor(.white, for: .normal)
+ setTitleColor(.white, for: .disabled)
+ case .secondary:
+ backgroundColor = AppColor.secondaryButtonBackground
+ setTitleColor(AppColor.primary, for: .normal)
+ setTitleColor(AppColor.primary, for: .disabled)
+ alpha = isEnabled ? 1 : 0.6
+ }
+ }
+}
diff --git a/suixinkan/UI/Common/LoginTextField.swift b/suixinkan/UI/Common/LoginTextField.swift
new file mode 100644
index 0000000..a291a22
--- /dev/null
+++ b/suixinkan/UI/Common/LoginTextField.swift
@@ -0,0 +1,83 @@
+//
+// LoginTextField.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 登录页账号输入框,对应 Android `PhoneInputField`。
+final class LoginTextField: UIView {
+
+ private let textField = UITextField()
+
+ var text: String {
+ get { textField.text ?? "" }
+ set { textField.text = newValue }
+ }
+
+ var onTextChange: ((String) -> Void)?
+ var onReturnKey: (() -> Void)?
+
+ init(placeholder: String) {
+ super.init(frame: .zero)
+ setupUI(placeholder: placeholder)
+ setupConstraints()
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @discardableResult
+ override func becomeFirstResponder() -> Bool {
+ textField.becomeFirstResponder()
+ }
+
+ private func setupUI(placeholder: String) {
+ backgroundColor = AppColor.inputBackground
+ layer.cornerRadius = 12
+ clipsToBounds = true
+
+ textField.font = .systemFont(ofSize: 14)
+ textField.textColor = AppColor.text333
+ textField.autocapitalizationType = .none
+ textField.autocorrectionType = .no
+ textField.returnKeyType = .next
+ textField.clearButtonMode = .whileEditing
+ textField.delegate = self
+
+ let attributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: AppColor.placeholder,
+ .font: UIFont.systemFont(ofSize: 14),
+ ]
+ textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes)
+
+ textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
+ addSubview(textField)
+ }
+
+ private func setupConstraints() {
+ snp.makeConstraints { make in
+ make.height.equalTo(46)
+ }
+
+ textField.snp.makeConstraints { make in
+ make.leading.trailing.equalToSuperview().inset(16)
+ make.centerY.equalToSuperview()
+ }
+ }
+
+ @objc private func textDidChange() {
+ onTextChange?(text)
+ }
+}
+
+extension LoginTextField: UITextFieldDelegate {
+
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+ onReturnKey?()
+ return true
+ }
+}
diff --git a/suixinkan/UI/Common/PasswordInputField.swift b/suixinkan/UI/Common/PasswordInputField.swift
new file mode 100644
index 0000000..bd402d6
--- /dev/null
+++ b/suixinkan/UI/Common/PasswordInputField.swift
@@ -0,0 +1,104 @@
+//
+// PasswordInputField.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 登录页密码输入框,对应 Android `PasswordInputField`。
+final class PasswordInputField: UIView {
+
+ private let textField = UITextField()
+ private let toggleButton = UIButton(type: .custom)
+
+ private var isPasswordVisible = false
+
+ var text: String {
+ get { textField.text ?? "" }
+ set { textField.text = newValue }
+ }
+
+ var onTextChange: ((String) -> Void)?
+ var onReturnKey: (() -> Void)?
+
+ init(placeholder: String = "请输入密码") {
+ super.init(frame: .zero)
+ setupUI(placeholder: placeholder)
+ setupConstraints()
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @discardableResult
+ override func becomeFirstResponder() -> Bool {
+ textField.becomeFirstResponder()
+ }
+
+ private func setupUI(placeholder: String) {
+ backgroundColor = AppColor.inputBackground
+ layer.cornerRadius = 12
+ clipsToBounds = true
+
+ textField.font = .systemFont(ofSize: 14)
+ textField.textColor = AppColor.text333
+ textField.isSecureTextEntry = true
+ textField.autocapitalizationType = .none
+ textField.autocorrectionType = .no
+ textField.returnKeyType = .done
+ textField.delegate = self
+
+ let attributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: AppColor.placeholder,
+ .font: UIFont.systemFont(ofSize: 14),
+ ]
+ textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes)
+ textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
+
+ toggleButton.setImage(UIImage(named: "icon_pwd_invisible"), for: .normal)
+ toggleButton.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside)
+
+ addSubview(textField)
+ addSubview(toggleButton)
+ }
+
+ private func setupConstraints() {
+ snp.makeConstraints { make in
+ make.height.equalTo(46)
+ }
+
+ toggleButton.snp.makeConstraints { make in
+ make.trailing.equalToSuperview().inset(8)
+ make.centerY.equalToSuperview()
+ make.size.equalTo(30)
+ }
+
+ textField.snp.makeConstraints { make in
+ make.leading.equalToSuperview().inset(16)
+ make.trailing.equalTo(toggleButton.snp.leading).offset(-4)
+ make.centerY.equalToSuperview()
+ }
+ }
+
+ @objc private func textDidChange() {
+ onTextChange?(text)
+ }
+
+ @objc private func togglePasswordVisibility() {
+ isPasswordVisible.toggle()
+ textField.isSecureTextEntry = !isPasswordVisible
+ let imageName = isPasswordVisible ? "icon_pwd_visible" : "icon_pwd_invisible"
+ toggleButton.setImage(UIImage(named: imageName), for: .normal)
+ }
+}
+
+extension PasswordInputField: UITextFieldDelegate {
+
+ func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+ onReturnKey?()
+ return true
+ }
+}
diff --git a/suixinkan/UI/Home/HomeViewController.swift b/suixinkan/UI/Home/HomeViewController.swift
new file mode 100644
index 0000000..5f3929f
--- /dev/null
+++ b/suixinkan/UI/Home/HomeViewController.swift
@@ -0,0 +1,32 @@
+//
+// HomeViewController.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 首页 Tab 根页面。
+final class HomeViewController: BaseViewController {
+
+ private let titleLabel = UILabel()
+
+ override func setupNavigationBar() {
+ navigationController?.setNavigationBarHidden(true, animated: false)
+ }
+
+ override func setupUI() {
+ view.backgroundColor = UIColor(hex: 0xF7FAFF)
+ titleLabel.text = "首页"
+ titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
+ titleLabel.textAlignment = .center
+ titleLabel.textColor = AppColor.text333
+ view.addSubview(titleLabel)
+ }
+
+ override func setupConstraints() {
+ titleLabel.snp.makeConstraints { make in
+ make.center.equalToSuperview()
+ }
+ }
+}
diff --git a/suixinkan/UI/Login/LoginViewController.swift b/suixinkan/UI/Login/LoginViewController.swift
new file mode 100644
index 0000000..0f33604
--- /dev/null
+++ b/suixinkan/UI/Login/LoginViewController.swift
@@ -0,0 +1,224 @@
+//
+// LoginViewController.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 登录页,UI 严格对齐 Android `LoginScreen`。
+final class LoginViewController: BaseViewController {
+
+ private let viewModel = LoginViewModel()
+
+ private let backgroundImageView = UIImageView()
+ private let welcomeLabel = UILabel()
+ private let formCardView = UIView()
+ private let topFormStack = UIStackView()
+ private let accountField = LoginTextField(placeholder: "请输入账号")
+ private let passwordField = PasswordInputField()
+ private let agreementRow = AgreementRowView()
+ private let loginButton = AppButton(title: "登录", style: .primary)
+ private let loginByCodeButton = AppButton(title: "验证码登录", style: .secondary)
+ private let registerContainer = UIStackView()
+ private let registerPrefixLabel = UILabel()
+ private let registerButton = UIButton(type: .system)
+
+ private var hasPlayedEntranceAnimation = false
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ .lightContent
+ }
+
+ override func setupNavigationBar() {
+ navigationController?.setNavigationBarHidden(true, animated: false)
+ }
+
+ override func setupUI() {
+ view.backgroundColor = .black
+
+ backgroundImageView.image = UIImage(named: "img_login_bg")
+ backgroundImageView.contentMode = .scaleAspectFill
+ backgroundImageView.clipsToBounds = true
+
+ welcomeLabel.text = "欢迎使用\n随心瞰商家版"
+ welcomeLabel.numberOfLines = 2
+ welcomeLabel.font = .systemFont(ofSize: 28, weight: .semibold)
+ welcomeLabel.textColor = .white
+
+ formCardView.backgroundColor = .white
+ formCardView.layer.cornerRadius = 16
+ formCardView.clipsToBounds = true
+
+ topFormStack.axis = .vertical
+ topFormStack.spacing = 0
+ topFormStack.alignment = .fill
+
+ loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
+ loginByCodeButton.addTarget(self, action: #selector(loginByCodeButtonTapped), for: .touchUpInside)
+
+ registerContainer.axis = .horizontal
+ registerContainer.alignment = .center
+ registerContainer.spacing = 0
+
+ registerPrefixLabel.text = "没有账号?"
+ registerPrefixLabel.font = .systemFont(ofSize: 14, weight: .medium)
+ registerPrefixLabel.textColor = AppColor.text333
+
+ registerButton.setTitle("去注册", for: .normal)
+ registerButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
+ registerButton.setTitleColor(AppColor.primary, for: .normal)
+ registerButton.addTarget(self, action: #selector(registerButtonTapped), for: .touchUpInside)
+
+ registerContainer.addArrangedSubview(registerPrefixLabel)
+ registerContainer.addArrangedSubview(registerButton)
+
+ view.addSubview(backgroundImageView)
+ view.addSubview(welcomeLabel)
+ view.addSubview(formCardView)
+ formCardView.addSubview(topFormStack)
+ formCardView.addSubview(registerContainer)
+
+ topFormStack.addArrangedSubview(accountField)
+ topFormStack.setCustomSpacing(8, after: accountField)
+ topFormStack.addArrangedSubview(passwordField)
+ topFormStack.setCustomSpacing(24, after: passwordField)
+ topFormStack.addArrangedSubview(agreementRow)
+ topFormStack.setCustomSpacing(16, after: agreementRow)
+ topFormStack.addArrangedSubview(loginButton)
+ topFormStack.setCustomSpacing(8, after: loginButton)
+ topFormStack.addArrangedSubview(loginByCodeButton)
+
+ bindViewModel()
+ updateUI()
+ }
+
+ override func setupConstraints() {
+ backgroundImageView.snp.makeConstraints { make in
+ make.edges.equalToSuperview()
+ }
+
+ welcomeLabel.snp.makeConstraints { make in
+ make.top.equalTo(view.safeAreaLayoutGuide).offset(48)
+ make.leading.trailing.equalToSuperview().inset(24)
+ }
+
+ formCardView.snp.makeConstraints { make in
+ make.top.equalTo(welcomeLabel.snp.bottom).offset(48)
+ make.leading.trailing.equalToSuperview().inset(24)
+ make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-24)
+ }
+
+ topFormStack.snp.makeConstraints { make in
+ make.top.leading.trailing.equalToSuperview().inset(16)
+ make.bottom.equalToSuperview().inset(16)
+ }
+
+ registerContainer.snp.makeConstraints { make in
+ make.centerX.equalToSuperview()
+ make.bottom.equalToSuperview().inset(16)
+ }
+ }
+
+ override func bindActions() {
+ accountField.onTextChange = { [weak self] text in
+ self?.viewModel.updateAccount(text)
+ }
+ accountField.onReturnKey = { [weak self] in
+ self?.passwordField.becomeFirstResponder()
+ }
+
+ passwordField.onTextChange = { [weak self] text in
+ self?.viewModel.updatePassword(text)
+ }
+ passwordField.onReturnKey = { [weak self] in
+ self?.view.endEditing(true)
+ self?.viewModel.login()
+ }
+
+ agreementRow.onCheckedChange = { [weak self] in
+ self?.viewModel.togglePrivacyChecked()
+ }
+ agreementRow.onUserAgreementTap = { [weak self] in
+ self?.viewModel.onUserAgreement?()
+ }
+ agreementRow.onPrivacyPolicyTap = { [weak self] in
+ self?.viewModel.onPrivacyPolicy?()
+ }
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ playEntranceAnimationIfNeeded()
+ }
+
+ private func bindViewModel() {
+ viewModel.onStateChange = { [weak self] in
+ self?.updateUI()
+ }
+ viewModel.onShowMessage = { [weak self] message in
+ self?.showToast(message)
+ }
+ viewModel.onLogin = { [weak self] in
+ self?.showToast("登录功能待接入")
+ }
+ viewModel.onLoginByCode = { [weak self] in
+ self?.showToast("验证码登录待接入")
+ }
+ viewModel.onRegister = { [weak self] in
+ self?.showToast("注册待接入")
+ }
+ viewModel.onUserAgreement = { [weak self] in
+ self?.showToast("用户协议待接入")
+ }
+ viewModel.onPrivacyPolicy = { [weak self] in
+ self?.showToast("隐私政策待接入")
+ }
+ }
+
+ private func updateUI() {
+ agreementRow.isChecked = viewModel.isPrivacyChecked
+ loginButton.isEnabled = viewModel.isLoginEnabled
+
+ let showRegisterSection = viewModel.enableRegister
+ loginByCodeButton.isHidden = !showRegisterSection
+ registerContainer.isHidden = !showRegisterSection
+
+ topFormStack.snp.remakeConstraints { make in
+ make.top.leading.trailing.equalToSuperview().inset(16)
+ if showRegisterSection {
+ make.bottom.lessThanOrEqualTo(registerContainer.snp.top).offset(-16)
+ } else {
+ make.bottom.equalToSuperview().inset(16)
+ }
+ }
+ }
+
+ private func playEntranceAnimationIfNeeded() {
+ guard !hasPlayedEntranceAnimation else { return }
+ hasPlayedEntranceAnimation = true
+
+ formCardView.transform = CGAffineTransform(translationX: 0, y: view.bounds.height)
+ welcomeLabel.alpha = 0
+
+ UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) {
+ self.formCardView.transform = .identity
+ self.welcomeLabel.alpha = 1
+ }
+ }
+
+ @objc private func loginButtonTapped() {
+ view.endEditing(true)
+ viewModel.login()
+ }
+
+ @objc private func loginByCodeButtonTapped() {
+ view.endEditing(true)
+ viewModel.onLoginByCode?()
+ }
+
+ @objc private func registerButtonTapped() {
+ view.endEditing(true)
+ viewModel.onRegister?()
+ }
+}
diff --git a/suixinkan/UI/Login/LoginViewModel.swift b/suixinkan/UI/Login/LoginViewModel.swift
new file mode 100644
index 0000000..520cbed
--- /dev/null
+++ b/suixinkan/UI/Login/LoginViewModel.swift
@@ -0,0 +1,61 @@
+//
+// LoginViewModel.swift
+// suixinkan
+//
+
+import Foundation
+
+/// 登录页 ViewModel,UI 状态与 Android `LoginViewModel` 对齐。
+final class LoginViewModel {
+
+ private(set) var account = ""
+ private(set) var password = ""
+ private(set) var isPrivacyChecked = false
+ private(set) var enableRegister = false
+
+ var isLoginEnabled: Bool {
+ !account.isEmpty && !password.isEmpty
+ }
+
+ var onStateChange: (() -> Void)?
+
+ func updateAccount(_ value: String) {
+ account = value
+ notifyStateChange()
+ }
+
+ func updatePassword(_ value: String) {
+ password = value
+ notifyStateChange()
+ }
+
+ func togglePrivacyChecked() {
+ isPrivacyChecked.toggle()
+ notifyStateChange()
+ }
+
+ func updateEnableRegister(_ enabled: Bool) {
+ enableRegister = enabled
+ notifyStateChange()
+ }
+
+ func login() {
+ guard isPrivacyChecked else {
+ onShowMessage?("请先阅读并同意用户协议与隐私政策")
+ return
+ }
+ guard isLoginEnabled else { return }
+ onLogin?()
+ }
+
+ var onShowMessage: ((String) -> Void)?
+ var onLogin: (() -> Void)?
+ var onLoginByCode: (() -> Void)?
+ var onRegister: (() -> Void)?
+ var onUserAgreement: (() -> Void)?
+ var onPrivacyPolicy: (() -> Void)?
+
+ private func notifyStateChange() {
+ onStateChange?()
+ }
+}
diff --git a/suixinkan/UI/MainTab/AppTab.swift b/suixinkan/UI/MainTab/AppTab.swift
new file mode 100644
index 0000000..e60c9e6
--- /dev/null
+++ b/suixinkan/UI/MainTab/AppTab.swift
@@ -0,0 +1,64 @@
+//
+// AppTab.swift
+// suixinkan
+//
+
+import UIKit
+
+/// 主 Tab 实体,描述底部导航中的一级入口。
+enum AppTab: String, CaseIterable, Hashable {
+ case home
+ case orders
+ case statistics
+ case profile
+
+ var title: String {
+ switch self {
+ case .home: "首页"
+ case .orders: "订单"
+ case .statistics: "数据"
+ case .profile: "我的"
+ }
+ }
+
+ var selectedImageName: String {
+ switch self {
+ case .home: "tab_home_selected"
+ case .orders: "tab_order_selected"
+ case .statistics: "tab_data_selected"
+ case .profile: "tab_profile_selected"
+ }
+ }
+
+ var unselectedImageName: String {
+ switch self {
+ case .home: "tab_home_unselected"
+ case .orders: "tab_order_unselected"
+ case .statistics: "tab_data_unselected"
+ case .profile: "tab_profile_unselected"
+ }
+ }
+
+ /// 构建系统 TabBarItem。
+ func makeTabBarItem() -> UITabBarItem {
+ let unselectedImage = UIImage(named: unselectedImageName)?.withRenderingMode(.alwaysOriginal)
+ let selectedImage = UIImage(named: selectedImageName)?.withRenderingMode(.alwaysOriginal)
+ let item = UITabBarItem(title: title, image: unselectedImage, selectedImage: selectedImage)
+ item.accessibilityIdentifier = "main.tab.\(rawValue)"
+ return item
+ }
+
+ /// 构建 Tab 根页面。
+ func makeRootViewController() -> UIViewController {
+ switch self {
+ case .home:
+ return HomeViewController()
+ case .orders:
+ return OrdersViewController()
+ case .statistics:
+ return StatisticsViewController()
+ case .profile:
+ return ProfileViewController()
+ }
+ }
+}
diff --git a/suixinkan/UI/MainTab/MainScanTabItem.swift b/suixinkan/UI/MainTab/MainScanTabItem.swift
new file mode 100644
index 0000000..ebf7792
--- /dev/null
+++ b/suixinkan/UI/MainTab/MainScanTabItem.swift
@@ -0,0 +1,19 @@
+//
+// MainScanTabItem.swift
+// suixinkan
+//
+
+import UIKit
+
+/// 主 TabBar 中间扫码核销入口。
+enum MainScanTabItem {
+
+ /// 构建扫码 TabBarItem,点击行为由 `MainTabBarController` 拦截。
+ static func makeTabBarItem() -> UITabBarItem {
+ let image = UIImage(named: "icon_scan")?.withRenderingMode(.alwaysOriginal)
+ let item = UITabBarItem(title: nil, image: image, selectedImage: image)
+ item.accessibilityLabel = "扫码核销"
+ item.accessibilityIdentifier = "main.scan"
+ return item
+ }
+}
diff --git a/suixinkan/UI/MainTab/MainTabBadgeViewModel.swift b/suixinkan/UI/MainTab/MainTabBadgeViewModel.swift
new file mode 100644
index 0000000..5597684
--- /dev/null
+++ b/suixinkan/UI/MainTab/MainTabBadgeViewModel.swift
@@ -0,0 +1,21 @@
+//
+// MainTabBadgeViewModel.swift
+// suixinkan
+//
+
+import Foundation
+
+/// 主 Tab 角标 ViewModel,负责订单 Tab 待核销数量。
+final class MainTabBadgeViewModel {
+
+ var onChange: (() -> Void)?
+
+ private(set) var pendingWriteOffCount: Int? {
+ didSet { onChange?() }
+ }
+
+ /// 刷新待核销数量;接口未接入前保持为空。
+ func refreshPendingWriteOffCount() {
+ pendingWriteOffCount = nil
+ }
+}
diff --git a/suixinkan/UI/MainTab/MainTabBarController.swift b/suixinkan/UI/MainTab/MainTabBarController.swift
new file mode 100644
index 0000000..a2309f7
--- /dev/null
+++ b/suixinkan/UI/MainTab/MainTabBarController.swift
@@ -0,0 +1,154 @@
+//
+// MainTabBarController.swift
+// suixinkan
+//
+
+import UIKit
+
+/// 主 Tab 容器,展示四个一级入口及中间扫码按钮。
+final class MainTabBarController: UITabBarController {
+
+ private let badgeViewModel = MainTabBadgeViewModel()
+ private var tabNavigationControllers: [AppTab: TabNavigationController] = [:]
+ private var isSyncingTabSelection = false
+
+ /// 测试时可注入扫码点击处理。
+ var onScanTabSelected: (() -> Void)?
+
+ private enum TabSlot: CaseIterable {
+ case home
+ case orders
+ case scan
+ case statistics
+ case profile
+
+ var appTab: AppTab? {
+ switch self {
+ case .home: return .home
+ case .orders: return .orders
+ case .scan: return nil
+ case .statistics: return .statistics
+ case .profile: return .profile
+ }
+ }
+
+ static func index(for tab: AppTab) -> Int? {
+ allCases.firstIndex { $0.appTab == tab }
+ }
+
+ static var scanIndex: Int? {
+ allCases.firstIndex(of: .scan)
+ }
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ delegate = self
+ configureTabBarAppearance()
+ configureTabs()
+ bindBadgeViewModel()
+ badgeViewModel.refreshPendingWriteOffCount()
+ }
+
+ /// 选中指定主 Tab。
+ func selectTab(_ tab: AppTab) {
+ guard let index = TabSlot.index(for: tab) else { return }
+ guard selectedIndex != index else { return }
+ isSyncingTabSelection = true
+ selectedIndex = index
+ isSyncingTabSelection = false
+ if tab == .orders {
+ badgeViewModel.refreshPendingWriteOffCount()
+ }
+ }
+
+ private func configureTabBarAppearance() {
+ tabBar.tintColor = AppColor.primary
+ tabBar.backgroundColor = .white
+ tabBar.shadowImage = UIImage()
+
+ let normalAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: UIColor(hex: 0x7D8DA3),
+ .font: UIFont.systemFont(ofSize: 12, weight: .regular),
+ ]
+ let selectedAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: AppColor.primary,
+ .font: UIFont.systemFont(ofSize: 12, weight: .medium),
+ ]
+
+ let appearance = UITabBarAppearance()
+ appearance.configureWithOpaqueBackground()
+ appearance.backgroundColor = .white
+ appearance.shadowColor = .clear
+ appearance.stackedLayoutAppearance.normal.titleTextAttributes = normalAttributes
+ appearance.stackedLayoutAppearance.selected.titleTextAttributes = selectedAttributes
+ tabBar.standardAppearance = appearance
+ tabBar.scrollEdgeAppearance = appearance
+ }
+
+ private func configureTabs() {
+ tabNavigationControllers.removeAll()
+ viewControllers = TabSlot.allCases.map { slot in
+ if let tab = slot.appTab {
+ let navigationController = TabNavigationController(tab: tab)
+ navigationController.tabBarItem = tab.makeTabBarItem()
+ tabNavigationControllers[tab] = navigationController
+ return navigationController
+ }
+
+ let scannerPlaceholder = UIViewController()
+ scannerPlaceholder.tabBarItem = MainScanTabItem.makeTabBarItem()
+ return scannerPlaceholder
+ }
+ }
+
+ private func bindBadgeViewModel() {
+ badgeViewModel.onChange = { [weak self] in
+ self?.updateOrderBadge()
+ }
+ }
+
+ private func updateOrderBadge() {
+ let badgeValue: String?
+ if let count = badgeViewModel.pendingWriteOffCount, count > 0 {
+ badgeValue = "\(count)"
+ } else {
+ badgeValue = nil
+ }
+ tabNavigationControllers[.orders]?.tabBarItem.badgeValue = badgeValue
+ }
+
+ private func presentGlobalScanner() {
+ guard presentedViewController == nil else { return }
+
+ let scanner = ScanPlaceholderViewController()
+ let navigationController = UINavigationController(rootViewController: scanner)
+ navigationController.modalPresentationStyle = .fullScreen
+ present(navigationController, animated: true)
+ }
+}
+
+extension MainTabBarController: UITabBarControllerDelegate {
+
+ func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
+ guard viewControllers?.firstIndex(of: viewController) == TabSlot.scanIndex else {
+ return true
+ }
+
+ if let onScanTabSelected {
+ onScanTabSelected()
+ } else {
+ presentGlobalScanner()
+ }
+ return false
+ }
+
+ func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
+ guard !isSyncingTabSelection,
+ let navigationController = viewController as? TabNavigationController else { return }
+
+ if navigationController.appTab == .orders {
+ badgeViewModel.refreshPendingWriteOffCount()
+ }
+ }
+}
diff --git a/suixinkan/UI/MainTab/TabNavigationController.swift b/suixinkan/UI/MainTab/TabNavigationController.swift
new file mode 100644
index 0000000..63e946a
--- /dev/null
+++ b/suixinkan/UI/MainTab/TabNavigationController.swift
@@ -0,0 +1,73 @@
+//
+// TabNavigationController.swift
+// suixinkan
+//
+
+import UIKit
+
+/// 单个 Tab 的导航栈。
+final class TabNavigationController: UINavigationController {
+
+ let appTab: AppTab
+
+ init(tab: AppTab) {
+ self.appTab = tab
+ super.init(nibName: nil, bundle: nil)
+ delegate = self
+ navigationBar.prefersLargeTitles = false
+ setViewControllers([tab.makeRootViewController()], animated: false)
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func pushViewController(_ viewController: UIViewController, animated: Bool) {
+ viewController.hidesBottomBarWhenPushed = viewControllers.count > 0
+ updateNavigationBarVisibility(for: viewController, animated: animated)
+ super.pushViewController(viewController, animated: animated)
+ }
+
+ override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
+ if let topViewController = viewControllers.last {
+ updateNavigationBarVisibility(for: topViewController, animated: animated)
+ }
+ super.setViewControllers(viewControllers, animated: animated)
+ }
+
+ override func popViewController(animated: Bool) -> UIViewController? {
+ let poppedViewController = super.popViewController(animated: animated)
+ if !animated, let topViewController {
+ updateNavigationBarVisibility(for: topViewController, animated: false)
+ }
+ return poppedViewController
+ }
+
+ override func popToRootViewController(animated: Bool) -> [UIViewController]? {
+ let poppedViewControllers = super.popToRootViewController(animated: animated)
+ if !animated, let topViewController {
+ updateNavigationBarVisibility(for: topViewController, animated: false)
+ }
+ return poppedViewControllers
+ }
+
+ private func shouldHideNavigationBar(for viewController: UIViewController) -> Bool {
+ viewController is HomeViewController
+ }
+
+ private func updateNavigationBarVisibility(for viewController: UIViewController, animated: Bool) {
+ setNavigationBarHidden(shouldHideNavigationBar(for: viewController), animated: animated)
+ }
+}
+
+extension TabNavigationController: UINavigationControllerDelegate {
+
+ func navigationController(
+ _ navigationController: UINavigationController,
+ willShow viewController: UIViewController,
+ animated: Bool
+ ) {
+ updateNavigationBarVisibility(for: viewController, animated: animated)
+ }
+}
diff --git a/suixinkan/UI/Orders/OrdersViewController.swift b/suixinkan/UI/Orders/OrdersViewController.swift
new file mode 100644
index 0000000..84e48cc
--- /dev/null
+++ b/suixinkan/UI/Orders/OrdersViewController.swift
@@ -0,0 +1,28 @@
+//
+// OrdersViewController.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 订单 Tab 根页面。
+final class OrdersViewController: BaseViewController {
+
+ private let titleLabel = UILabel()
+
+ override func setupUI() {
+ title = "订单"
+ titleLabel.text = "订单"
+ titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
+ titleLabel.textAlignment = .center
+ titleLabel.textColor = AppColor.text333
+ view.addSubview(titleLabel)
+ }
+
+ override func setupConstraints() {
+ titleLabel.snp.makeConstraints { make in
+ make.center.equalToSuperview()
+ }
+ }
+}
diff --git a/suixinkan/UI/Profile/ProfileViewController.swift b/suixinkan/UI/Profile/ProfileViewController.swift
new file mode 100644
index 0000000..dcead50
--- /dev/null
+++ b/suixinkan/UI/Profile/ProfileViewController.swift
@@ -0,0 +1,28 @@
+//
+// ProfileViewController.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 我的 Tab 根页面。
+final class ProfileViewController: BaseViewController {
+
+ private let titleLabel = UILabel()
+
+ override func setupUI() {
+ title = "我的"
+ titleLabel.text = "我的"
+ titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
+ titleLabel.textAlignment = .center
+ titleLabel.textColor = AppColor.text333
+ view.addSubview(titleLabel)
+ }
+
+ override func setupConstraints() {
+ titleLabel.snp.makeConstraints { make in
+ make.center.equalToSuperview()
+ }
+ }
+}
diff --git a/suixinkan/UI/Scan/ScanPlaceholderViewController.swift b/suixinkan/UI/Scan/ScanPlaceholderViewController.swift
new file mode 100644
index 0000000..5f9fef2
--- /dev/null
+++ b/suixinkan/UI/Scan/ScanPlaceholderViewController.swift
@@ -0,0 +1,50 @@
+//
+// ScanPlaceholderViewController.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 扫码核销占位页,后续替换为真实扫码页。
+final class ScanPlaceholderViewController: BaseViewController {
+
+ private let titleLabel = UILabel()
+ private let closeButton = UIButton(type: .system)
+
+ override func setupNavigationBar() {
+ navigationController?.setNavigationBarHidden(true, animated: false)
+ }
+
+ override func setupUI() {
+ view.backgroundColor = .black
+
+ titleLabel.text = "扫码核销"
+ titleLabel.font = .systemFont(ofSize: 20, weight: .medium)
+ titleLabel.textColor = .white
+ titleLabel.textAlignment = .center
+
+ closeButton.setTitle("关闭", for: .normal)
+ closeButton.setTitleColor(.white, for: .normal)
+ closeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
+ closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
+
+ view.addSubview(titleLabel)
+ view.addSubview(closeButton)
+ }
+
+ override func setupConstraints() {
+ titleLabel.snp.makeConstraints { make in
+ make.center.equalToSuperview()
+ }
+
+ closeButton.snp.makeConstraints { make in
+ make.top.equalTo(view.safeAreaLayoutGuide).offset(12)
+ make.trailing.equalToSuperview().inset(16)
+ }
+ }
+
+ @objc private func closeTapped() {
+ dismiss(animated: true)
+ }
+}
diff --git a/suixinkan/UI/Statistics/StatisticsViewController.swift b/suixinkan/UI/Statistics/StatisticsViewController.swift
new file mode 100644
index 0000000..fcf9d49
--- /dev/null
+++ b/suixinkan/UI/Statistics/StatisticsViewController.swift
@@ -0,0 +1,28 @@
+//
+// StatisticsViewController.swift
+// suixinkan
+//
+
+import SnapKit
+import UIKit
+
+/// 数据 Tab 根页面。
+final class StatisticsViewController: BaseViewController {
+
+ private let titleLabel = UILabel()
+
+ override func setupUI() {
+ title = "数据"
+ titleLabel.text = "数据"
+ titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
+ titleLabel.textAlignment = .center
+ titleLabel.textColor = AppColor.text333
+ view.addSubview(titleLabel)
+ }
+
+ override func setupConstraints() {
+ titleLabel.snp.makeConstraints { make in
+ make.center.equalToSuperview()
+ }
+ }
+}
diff --git a/suixinkan/ViewController.swift b/suixinkan/ViewController.swift
deleted file mode 100644
index 7f6fc1e..0000000
--- a/suixinkan/ViewController.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// ViewController.swift
-// suixinkan
-//
-// Created by hanqiu on 2026/7/6.
-//
-
-import UIKit
-
-class ViewController: BaseViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
- }
-
-}
-