feat: 更新项目配置和功能模块

- 修改Package.swift以支持iOS 15和macOS 12。
- 更新swift-tca-architecture-guidelines.mdc中的alwaysApply设置为false。
- 注释掉AppDelegate中的NIMSDK导入,移除不再使用的NIMConfigurationManager和NIMSessionManager文件。
- 添加新的API相关文件,包括EMailLoginFeature、IDLoginFeature和相关视图,增强登录功能。
- 更新APIConstants和APIEndpoints以反映新的API路径。
- 添加本地化支持文件,包含英文和中文简体的本地化字符串。
- 新增字体管理和安全工具类,支持AES和DES加密。
- 更新Xcode项目配置,调整版本号和启动画面设置。
This commit is contained in:
edwinQQQ
2025-07-09 16:14:19 +08:00
parent 5926906f3c
commit c470dba79c
71 changed files with 4000 additions and 522 deletions

View File

@@ -0,0 +1,43 @@
import SwiftUI
import ComposableArchitecture
struct AppRootView: View {
@State private var shouldShowMainApp = false
let splashStore = Store(
initialState: SplashFeature.State()
) {
SplashFeature()
}
let loginStore = Store(
initialState: LoginFeature.State()
) {
LoginFeature()
}
var body: some View {
Group {
if shouldShowMainApp {
//
LoginView(store: loginStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
} else {
//
SplashView(store: splashStore)
.transition(.opacity.animation(.easeInOut(duration: 0.5)))
.onReceive(NotificationCenter.default.publisher(for: .splashFinished)) { _ in
shouldShowMainApp = true
}
}
}
}
}
extension Notification.Name {
static let splashFinished = Notification.Name("splashFinished")
}
#Preview {
AppRootView()
}

View File

@@ -0,0 +1,59 @@
import SwiftUI
// MARK: - Login Button Component
struct LoginButton: View {
let iconName: String
let iconColor: Color
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
ZStack {
//
Color.white
.cornerRadius(28)
//
Text(title)
.font(.system(size: 18, weight: .semibold))
.frame(alignment: .center)
.foregroundColor(Color(hex: 0x313131))
//
HStack {
Image(systemName: iconName)
.foregroundColor(iconColor)
.font(.system(size: 30))
.padding(.leading, 33)
Spacer()
}
}
.frame(height: 56)
.padding(.horizontal, 29)
}
}
}
#Preview {
VStack(spacing: 20) {
LoginButton(
iconName: "person.circle.fill",
iconColor: .green,
title: "ID Login"
) {
// Preview action
}
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: "Email Login"
) {
// Preview action
}
}
.padding()
.background(Color.gray.opacity(0.2))
}

View File

@@ -0,0 +1,88 @@
import SwiftUI
// MARK: - User Agreement View Component
struct UserAgreementView: View {
@Binding var isAgreed: Bool
let onUserServiceTapped: () -> Void
let onPrivacyPolicyTapped: () -> Void
var body: some View {
HStack(alignment: .center, spacing: 12) {
//
Button(action: {
isAgreed.toggle()
}) {
Image(systemName: isAgreed ? "checkmark.circle.fill" : "circle")
.font(.system(size: 22))
.foregroundColor(isAgreed ? Color(hex: 0x8A4FFF) : Color(hex: 0x666666))
}
//
Text(createAttributedText())
.font(.system(size: 14))
.multilineTextAlignment(.leading)
.environment(\.openURL, OpenURLAction { url in
if url.absoluteString == "user-service-agreement" {
onUserServiceTapped()
return .handled
} else if url.absoluteString == "privacy-policy" {
onPrivacyPolicyTapped()
return .handled
}
return .systemAction
})
}
.frame(maxWidth: .infinity) //
.padding(.horizontal, 29) //
}
// MARK: - Private Methods
private func createAttributedText() -> AttributedString {
var attributedString = AttributedString("login.agreement_policy".localized)
//
attributedString.foregroundColor = Color(hex: 0x666666)
// ""
if let userServiceRange = attributedString.range(of: "login.agreement".localized) {
attributedString[userServiceRange].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[userServiceRange].underlineStyle = .single
attributedString[userServiceRange].link = URL(string: "user-service-agreement")
}
// ""
if let privacyPolicyRange = attributedString.range(of: "login.policy".localized) {
attributedString[privacyPolicyRange].foregroundColor = Color(hex: 0x8A4FFF)
attributedString[privacyPolicyRange].underlineStyle = .single
attributedString[privacyPolicyRange].link = URL(string: "privacy-policy")
}
return attributedString
}
}
#Preview {
VStack(spacing: 20) {
UserAgreementView(
isAgreed: .constant(true),
onUserServiceTapped: {
print("User Service Agreement tapped")
},
onPrivacyPolicyTapped: {
print("Privacy Policy tapped")
}
)
UserAgreementView(
isAgreed: .constant(true),
onUserServiceTapped: {
print("User Service Agreement tapped")
},
onPrivacyPolicyTapped: {
print("Privacy Policy tapped")
}
)
}
.padding()
.background(Color.gray.opacity(0.1))
}

View File

@@ -0,0 +1,55 @@
import SwiftUI
import SafariServices
// MARK: - Web View Component
struct WebView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = false
config.barCollapsingEnabled = true
let safariViewController = SFSafariViewController(url: url, configuration: config)
safariViewController.preferredBarTintColor = UIColor.systemBackground
safariViewController.preferredControlTintColor = UIColor.systemBlue
return safariViewController
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) {
// Safari View Controller
}
}
// MARK: - Web View Modifier
extension View {
/// Web
/// - Parameters:
/// - isPresented:
/// - url: URL
/// - Returns:
func webView(isPresented: Binding<Bool>, url: URL?) -> some View {
self.sheet(isPresented: isPresented) {
if let url = url {
WebView(url: url)
} else {
Text("无法加载页面")
.foregroundColor(.red)
.padding()
}
}
}
}
#Preview {
VStack {
Button("打开网页") {
//
}
}
.webView(
isPresented: .constant(true),
url: URL(string: "https://www.apple.com")
)
}

View File

@@ -0,0 +1,214 @@
import SwiftUI
import ComposableArchitecture
struct EMailLoginView: View {
let store: StoreOf<EMailLoginFeature>
let onBack: () -> Void
// 使@StateUI
@State private var email: String = ""
@State private var verificationCode: String = ""
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
}
//
private var getCodeButtonText: String {
if store.isCodeLoading {
return ""
} else if store.codeCountdown > 0 {
return "\(store.codeCountdown)S"
} else {
return "email_login.get_code".localized
}
}
var body: some View {
GeometryReader { geometry in
ZStack {
// - 使"bg"
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
HStack {
Button(action: {
onBack()
}) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 8)
Spacer()
.frame(height: 60)
//
Text("email_login.title".localized)
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 24) {
//
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
TextField("", text: $email)
.placeholder(when: email.isEmpty) {
Text("placeholder.enter_email".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
}
//
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text("placeholder.enter_verification_code".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.keyboardType(.numberPad)
//
Button(action: {
store.send(.getVerificationCodeTapped)
}) {
ZStack {
if store.isCodeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.7)
} else {
Text(getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
}
}
.frame(width: 60, height: 36)
.background(
RoundedRectangle(cornerRadius: 18)
.fill(Color.white.opacity(store.isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1))
)
}
.disabled(!store.isCodeButtonEnabled || email.isEmpty || store.isCodeLoading)
}
.padding(.horizontal, 24)
}
}
.padding(.horizontal, 32)
Spacer()
.frame(height: 60)
//
Button(action: {
// action
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? "email_login.logging_in".localized : "email_login.login_button".localized)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
// TCA
email = store.email
verificationCode = store.verificationCode
#if DEBUG
// Debug
if email.isEmpty {
email = "85494536@gmail.com"
}
if verificationCode.isEmpty {
verificationCode = "784544"
}
print("🐛 Debug模式: 默认邮箱=\(email), 默认验证码=\(verificationCode)")
#endif
}
}
}
#Preview {
EMailLoginView(
store: Store(
initialState: EMailLoginFeature.State()
) {
EMailLoginFeature()
},
onBack: {}
)
}

View File

@@ -0,0 +1,205 @@
import SwiftUI
import ComposableArchitecture
struct IDLoginView: View {
let store: StoreOf<IDLoginFeature>
let onBack: () -> Void
// 使@StateUI
@State private var userID: String = ""
@State private var password: String = ""
@State private var isPasswordVisible: Bool = false
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !userID.isEmpty && !password.isEmpty
}
var body: some View {
GeometryReader { geometry in
ZStack {
// - 使"bg"
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
HStack {
Button(action: {
onBack()
}) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 8)
Spacer()
.frame(height: 60)
//
Text("id_login.title".localized)
.font(.system(size: 28, weight: .medium))
.foregroundColor(.white)
.padding(.bottom, 80)
//
VStack(spacing: 24) {
// ID
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
TextField("", text: $userID) // 使SwiftUI
.placeholder(when: userID.isEmpty) {
Text("placeholder.enter_id".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.numberPad)
}
//
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
if isPasswordVisible {
TextField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
} else {
SecureField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text("placeholder.enter_password".localized)
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
.font(.system(size: 18))
}
}
.padding(.horizontal, 24)
}
}
.padding(.horizontal, 32)
// Forgot Password
HStack {
Spacer()
Button(action: {
store.send(.forgotPasswordTapped)
}) {
Text("id_login.forgot_password".localized)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
}
.padding(.horizontal, 32)
.padding(.top, 16)
Spacer()
.frame(height: 60)
//
Button(action: {
// action
store.send(.loginButtonTapped(userID: userID, password: password))
}) {
ZStack {
//
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0), // #D85EFF
Color(red: 0.54, green: 0.31, blue: 1.0) // #8A4FFF
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? "id_login.logging_in".localized : "id_login.login_button".localized)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || userID.isEmpty || password.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 50%
.padding(.horizontal, 32)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
}
}
.onAppear {
// TCA
userID = store.userID
password = store.password
isPasswordVisible = store.isPasswordVisible
#if DEBUG
//
print("🐛 Debug模式: 已移除硬编码测试凭据")
#endif
}
}
}
#Preview {
IDLoginView(
store: Store(
initialState: IDLoginFeature.State()
) {
IDLoginFeature()
},
onBack: {}
)
}

View File

@@ -0,0 +1,96 @@
import SwiftUI
import ComposableArchitecture
struct LanguageSettingsView: View {
@ObservedObject private var localizationManager = LocalizationManager.shared
@Binding var isPresented: Bool
init(isPresented: Binding<Bool> = .constant(true)) {
self._isPresented = isPresented
}
var body: some View {
NavigationView {
List {
Section {
ForEach(LocalizationManager.SupportedLanguage.allCases, id: \.rawValue) { language in
LanguageRow(
language: language,
isSelected: localizationManager.currentLanguage == language
) {
localizationManager.switchLanguage(to: language)
}
}
} header: {
Text("选择语言 / Select Language")
.font(.caption)
.foregroundColor(.secondary)
}
Section {
HStack {
Text("当前语言 / Current Language")
.font(.body)
Spacer()
Text(localizationManager.currentLanguage.localizedDisplayName)
.font(.body)
.foregroundColor(.blue)
}
} header: {
Text("语言信息 / Language Info")
.font(.caption)
.foregroundColor(.secondary)
}
}
.navigationTitle("语言设置 / Language")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("返回 / Back") {
isPresented = false
}
}
}
}
}
}
struct LanguageRow: View {
let language: LocalizationManager.SupportedLanguage
let isSelected: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(language.localizedDisplayName)
.font(.body)
.foregroundColor(.primary)
Text(language.displayName)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if isSelected {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.blue)
.font(.system(size: 20))
}
}
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
}
}
// MARK: - Preview
#Preview {
LanguageSettingsView(isPresented: .constant(true))
}

160
yana/Views/LoginView.swift Normal file
View File

@@ -0,0 +1,160 @@
import SwiftUI
import ComposableArchitecture
// PreferenceKey
struct ImageHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct LoginView: View {
let store: StoreOf<LoginFeature>
@State private var topImageHeight: CGFloat = 120 //
@ObservedObject private var localizationManager = LocalizationManager.shared
@State private var showLanguageSettings = false
@State private var isAgreedToTerms = true
@State private var showUserAgreement = false
@State private var showPrivacyPolicy = false
@State private var showIDLogin = false // 使SwiftUI@State
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
// 使 splash
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 0) {
// "top"
ZStack {
Image("top")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.padding(.top, -100)
.background(
GeometryReader { topImageGeometry in
Color.clear.preference(key: ImageHeightPreferenceKey.self, value: topImageGeometry.size.height)
}
)
// E-PARTI "top"20
HStack {
Text("login.app_title".localized)
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
.foregroundColor(.white)
.padding(.leading, 20)
Spacer()
}
.padding(.top, max(0, topImageHeight - 100)) // top - 140
// - Debug
#if DEBUG
VStack {
HStack {
Spacer()
Button(action: {
showLanguageSettings = true
}) {
Image(systemName: "globe")
.frame(width: 40, height: 40)
.font(.system(size: 20))
.foregroundColor(.white)
.background(Color.black.opacity(0.3))
.clipShape(Circle())
}
.padding(.trailing, 16)
}
Spacer()
}
#endif
VStack(spacing: 24) {
// ID Login
LoginButton(
iconName: "person.circle.fill",
iconColor: .green,
title: "login.id_login".localized
) {
showIDLogin = true // SwiftUI
}
// Email Login
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: "login.email_login".localized
) {
// TODO: Email
}
}.padding(.top, max(0, topImageHeight+140))
}
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
topImageHeight = imageHeight
}
// 使"top"40pt
Spacer()
.frame(height: 120)
//
UserAgreementView(
isAgreed: $isAgreedToTerms,
onUserServiceTapped: {
showUserAgreement = true
},
onPrivacyPolicyTapped: {
showPrivacyPolicy = true
}
)
.padding(.horizontal, 28)
.padding(.bottom, 140)
}
// NavigationLink - 使SwiftUI
NavigationLink(
destination: IDLoginView(
store: store.scope(
state: \.idLoginState,
action: \.idLogin
),
onBack: {
showIDLogin = false // SwiftUI
}
)
.navigationBarHidden(true),
isActive: $showIDLogin // 使SwiftUI
) {
EmptyView()
}
.hidden()
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
.sheet(isPresented: $showLanguageSettings) {
LanguageSettingsView(isPresented: $showLanguageSettings)
}
.webView(
isPresented: $showUserAgreement,
url: APIConfiguration.webURL(for: .userAgreement)
)
.webView(
isPresented: $showPrivacyPolicy,
url: APIConfiguration.webURL(for: .privacyPolicy)
)
}
}
#Preview {
LoginView(
store: Store(
initialState: LoginFeature.State()
) {
LoginFeature()
}
)
}

View File

@@ -0,0 +1,49 @@
import SwiftUI
import ComposableArchitecture
struct SplashView: View {
let store: StoreOf<SplashFeature>
var body: some View {
WithPerceptionTracking {
ZStack {
// -
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
VStack(spacing: 32) {
Spacer()
.frame(height: 200) // storyboard
// Logo - 100x100
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
// - 40pt
Text("E-Parti")
.font(.system(size: 40, weight: .regular))
.foregroundColor(.white)
Spacer()
}
}
.onAppear {
store.send(.onAppear)
}
}
}
}
#Preview {
SplashView(
store: Store(
initialState: SplashFeature.State()
) {
SplashFeature()
}
)
}