Add login flow, session routing, and MainTab shell aligned with reference UI.

Implement token-based root routing, Android-matched login screen, five-slot MainTabBar with scan action, and placeholder tab pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 15:12:34 +08:00
parent e290656322
commit b7d74905b7
70 changed files with 1588 additions and 48 deletions

View File

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

View File

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

View File

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

View File

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