diff --git a/.cursor/rules/swift-assistant-style.mdc b/.cursor/rules/swift-assistant-style.mdc index 90ed7fd..d648b3c 100644 --- a/.cursor/rules/swift-assistant-style.mdc +++ b/.cursor/rules/swift-assistant-style.mdc @@ -9,9 +9,9 @@ alwaysApply: true ## OBJECTIVE - As an expert AI programming assistant, your task is to provide me with clear and readable SwiftUI code. You should: + As an expert AI programming assistant, your task is to provide me with clear, readable, and effective code. You should: -- Utilize the latest versions of SwiftUI and Swift, being familiar with the newest features and best practices. +- Utilize the latest versions of SwiftUI, Swift(6) and TCA(1.20.2), being familiar with the newest features and best practices. - Provide careful and accurate answers that are well-founded and thoughtfully considered. - **Explicitly use the Chain-of-Thought (CoT) method in your reasoning and answers, explaining your thought process step by step.** - Strictly adhere to my requirements and meticulously complete the tasks. @@ -23,11 +23,7 @@ alwaysApply: true - Keep answers concise and direct, minimizing unnecessary wording. - Emphasize code readability over performance optimization. - Maintain a professional and supportive tone, ensuring clarity of content. - -## AUDIENCE - The target audience is me, a native Chinese developer eager to learn Swift 6 and Xcode 15.9, seeking guidance and advice on utilizing the latest technologies. - ## RESPONSE FORMAT - **Utilize the Chain-of-Thought (CoT) method to reason and respond, explaining your thought process step by step.** diff --git a/yana/Features/AppSettingFeature.swift b/yana/Features/AppSettingFeature.swift index 224694e..4664ba8 100644 --- a/yana/Features/AppSettingFeature.swift +++ b/yana/Features/AppSettingFeature.swift @@ -1,27 +1,114 @@ import Foundation import ComposableArchitecture -struct AppSettingFeature: Reducer { +@Reducer +struct AppSettingFeature { + @ObservableState struct State: Equatable { - var nickname: String = "hahahaha" + var nickname: String = "" var avatarURL: String? = nil + var userInfo: UserInfo? = nil + var isLoadingUserInfo: Bool = false + var userInfoError: String? = nil + + // WebView 导航状态 + var showUserAgreement: Bool = false + var showPrivacyPolicy: Bool = false } + enum Action: Equatable { case onAppear case editNicknameTapped case logoutTapped - // 可扩展更多 action + + // 用户信息相关 + case loadUserInfo + case userInfoLoaded(UserInfo?) + case userInfoLoadFailed(String) + + // WebView 导航 + case personalInfoPermissionsTapped + case helpTapped + case clearCacheTapped + case checkUpdatesTapped + case aboutUsTapped + + // WebView 关闭 + case userAgreementDismissed + case privacyPolicyDismissed } - func reduce(into state: inout State, action: Action) -> Effect { - switch action { - case .onAppear: - return .none - case .editNicknameTapped: - // 预留编辑昵称逻辑 - return .none - case .logoutTapped: - // 预留登出逻辑 - return .none + + @Dependency(\.apiService) var apiService + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .onAppear: + return .send(.loadUserInfo) + + case .editNicknameTapped: + // 预留编辑昵称逻辑 + return .none + + case .logoutTapped: + // 预留登出逻辑 + return .none + + case .loadUserInfo: + state.isLoadingUserInfo = true + state.userInfoError = nil + return .run { send in + let currentUid = await UserInfoManager.getCurrentUserId() + if let userInfo = await UserInfoManager.fetchUserInfoFromServer( + uid: currentUid, + apiService: apiService + ) { + await send(.userInfoLoaded(userInfo)) + } else { + await send(.userInfoLoadFailed("获取用户信息失败")) + } + } + + case let .userInfoLoaded(userInfo): + state.isLoadingUserInfo = false + state.userInfo = userInfo + state.nickname = userInfo?.nick ?? "hahahaha" + state.avatarURL = userInfo?.avatar + return .none + + case let .userInfoLoadFailed(error): + state.isLoadingUserInfo = false + state.userInfoError = error + return .none + + case .personalInfoPermissionsTapped: + state.showPrivacyPolicy = true + return .none + + case .helpTapped: + state.showUserAgreement = true + return .none + + case .clearCacheTapped: + // 预留清除缓存逻辑 + return .none + + case .checkUpdatesTapped: + // 预留检查更新逻辑 + return .none + + case .aboutUsTapped: + // 预留关于我们逻辑 + return .none + + case .userAgreementDismissed: + state.showUserAgreement = false + return .none + + case .privacyPolicyDismissed: + state.showPrivacyPolicy = false + return .none + } } } -} \ No newline at end of file +} diff --git a/yana/Resources/en.lproj/Localizable.strings b/yana/Resources/en.lproj/Localizable.strings index 75ce070..397d2e5 100644 --- a/yana/Resources/en.lproj/Localizable.strings +++ b/yana/Resources/en.lproj/Localizable.strings @@ -118,4 +118,15 @@ "setting.language" = "Language Settings"; "setting.about" = "About Us"; "setting.version" = "Version Info"; -"setting.logout" = "Logout"; \ No newline at end of file +"setting.logout" = "Logout"; + +// MARK: - App Setting +"appSetting.title" = "Edit"; +"appSetting.nickname" = "Nickname"; +"appSetting.personalInfoPermissions" = "Personal Information and Permissions"; +"appSetting.help" = "Help"; +"appSetting.clearCache" = "Clear Cache"; +"appSetting.checkUpdates" = "Check for Updates"; +"appSetting.logout" = "Log Out"; +"appSetting.aboutUs" = "About Us"; +"appSetting.logoutAccount" = "Log out of account"; \ No newline at end of file diff --git a/yana/Resources/zh-Hans.lproj/Localizable.strings b/yana/Resources/zh-Hans.lproj/Localizable.strings index 05ed160..857e5b1 100644 --- a/yana/Resources/zh-Hans.lproj/Localizable.strings +++ b/yana/Resources/zh-Hans.lproj/Localizable.strings @@ -115,3 +115,14 @@ "feed.2hoursago" = "2小时前"; "feed.demoContent" = "今天是美好的一天,分享一些生活中的小确幸。希望大家都能珍惜每一个当下的时刻。"; "feed.vip" = "VIP%d"; + +// MARK: - App Setting +"appSetting.title" = "编辑"; +"appSetting.nickname" = "昵称"; +"appSetting.personalInfoPermissions" = "个人信息与权限"; +"appSetting.help" = "帮助"; +"appSetting.clearCache" = "清除缓存"; +"appSetting.checkUpdates" = "检查更新"; +"appSetting.logout" = "退出登录"; +"appSetting.aboutUs" = "关于我们"; +"appSetting.logoutAccount" = "退出账户"; diff --git a/yana/Views/AppSettingView.swift b/yana/Views/AppSettingView.swift index 5227bc8..830fd29 100644 --- a/yana/Views/AppSettingView.swift +++ b/yana/Views/AppSettingView.swift @@ -8,80 +8,203 @@ struct AppSettingView: View { WithViewStore(self.store, observe: { $0 }) { viewStore in ZStack { Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea() - VStack(spacing: 0) { - Spacer().frame(height: 24) - // 顶部标题 - Text("Edit") - .font(.system(size: 22, weight: .semibold)) - .foregroundColor(.white) - .padding(.top, 8) - // 头像 - ZStack(alignment: .bottomTrailing) { - Image("avatar_placeholder") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 120, height: 120) - .clipShape(Circle()) - Button(action: {}) { - ZStack { - Circle().fill(Color.purple).frame(width: 36, height: 36) - Image(systemName: "camera.fill") - .foregroundColor(.white) - } - } - .offset(x: 8, y: 8) - } - .padding(.top, 24) - // 昵称 - HStack { - Text("Nickname") - .foregroundColor(.white) - Spacer() - Text(viewStore.nickname) - .foregroundColor(.gray) - Image(systemName: "chevron.right") - .foregroundColor(.gray) - } - .padding(.horizontal, 32) - .padding(.vertical, 18) - .onTapGesture { - viewStore.send(.editNicknameTapped) - } - Divider().background(Color.gray.opacity(0.3)) - .padding(.horizontal, 32) - // 其他设置项 +// VStack(spacing: 0) { + // 主要内容 VStack(spacing: 0) { - settingRow("Personal Information and Permissions") - settingRow("Help") - settingRow("Clear Cache") - settingRow("Check for Updates") - settingRow("Log Out") - settingRow("About Us") + // 头像区域 + avatarSection(viewStore: viewStore) + + // 昵称设置项 + nicknameSection(viewStore: viewStore) + + // 其他设置项 + settingsSection(viewStore: viewStore) + + Spacer() + + // 底部大按钮 + logoutButton(viewStore: viewStore) } - .background(Color.clear) - .padding(.horizontal, 0) - Spacer() - // 底部大按钮 - Button(action: { - viewStore.send(.logoutTapped) - }) { - Text("Log out of account") - .font(.system(size: 18, weight: .semibold)) +// } + } + .navigationTitle(NSLocalizedString("appSetting.title", comment: "Edit")) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + .toolbarColorScheme(.dark, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: {}) { + Image(systemName: "chevron.left") + .font(.system(size: 24, weight: .medium)) .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 18) - .background(Color.white.opacity(0.08)) - .cornerRadius(28) - .padding(.horizontal, 32) } - .padding(.bottom, 32) } } + .onAppear { + viewStore.send(.onAppear) + } + .webView( + isPresented: userAgreementBinding(viewStore: viewStore), + url: APIConfiguration.webURL(for: .userAgreement) + ) + .webView( + isPresented: privacyPolicyBinding(viewStore: viewStore), + url: APIConfiguration.webURL(for: .privacyPolicy) + ) } } - // 设置项行 - func settingRow(_ title: String) -> some View { - return VStack(spacing: 0) { + + + + // MARK: - 头像区域 + private func avatarSection(viewStore: ViewStoreOf) -> some View { + ZStack(alignment: .bottomTrailing) { + avatarImageView(viewStore: viewStore) + cameraButton + } + .padding(.top, 24) + } + + // MARK: - 头像图片视图 + @ViewBuilder + private func avatarImageView(viewStore: ViewStoreOf) -> some View { + if viewStore.isLoadingUserInfo { + loadingAvatarView + } else if let avatarURLString = viewStore.avatarURL, !avatarURLString.isEmpty, let avatarURL = URL(string: avatarURLString) { + networkAvatarView(url: avatarURL) + } else { + defaultAvatarView + } + } + + // MARK: - 加载状态头像 + private var loadingAvatarView: some View { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 120, height: 120) + .overlay( + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.2) + ) + } + + // MARK: - 网络头像 + private func networkAvatarView(url: URL) -> some View { + CachedAsyncImage(url: url.absoluteString) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + defaultAvatarView + } + .frame(width: 120, height: 120) + .clipShape(Circle()) + } + + // MARK: - 默认头像 + private var defaultAvatarView: some View { + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: 120, height: 120) + .overlay( + Image(systemName: "person.fill") + .font(.system(size: 40)) + .foregroundColor(.white) + ) + } + + // MARK: - 相机按钮 + private var cameraButton: some View { + Button(action: {}) { + ZStack { + Circle().fill(Color.purple).frame(width: 36, height: 36) + Image(systemName: "camera.fill") + .foregroundColor(.white) + } + } + .offset(x: 8, y: 8) + } + + // MARK: - 昵称设置项 + private func nicknameSection(viewStore: ViewStoreOf) -> some View { + VStack(spacing: 0) { + HStack { + Text(NSLocalizedString("appSetting.nickname", comment: "Nickname")) + .foregroundColor(.white) + Spacer() + Text(viewStore.nickname) + .foregroundColor(.gray) + Image(systemName: "chevron.right") + .foregroundColor(.gray) + } + .padding(.horizontal, 32) + .padding(.vertical, 18) + .onTapGesture { + viewStore.send(.editNicknameTapped) + } + + Divider().background(Color.gray.opacity(0.3)) + .padding(.horizontal, 32) + } + } + + // MARK: - 设置项区域 + private func settingsSection(viewStore: ViewStoreOf) -> some View { + VStack(spacing: 0) { + personalInfoPermissionsRow(viewStore: viewStore) + helpRow(viewStore: viewStore) + clearCacheRow(viewStore: viewStore) + checkUpdatesRow(viewStore: viewStore) + aboutUsRow(viewStore: viewStore) + } + .background(Color.clear) + .padding(.horizontal, 0) + } + + // MARK: - 个人信息权限行 + private func personalInfoPermissionsRow(viewStore: ViewStoreOf) -> some View { + settingRow( + title: NSLocalizedString("appSetting.personalInfoPermissions", comment: "Personal Information and Permissions"), + action: { viewStore.send(.personalInfoPermissionsTapped) } + ) + } + + // MARK: - 帮助行 + private func helpRow(viewStore: ViewStoreOf) -> some View { + settingRow( + title: NSLocalizedString("appSetting.help", comment: "Help"), + action: { viewStore.send(.helpTapped) } + ) + } + + // MARK: - 清除缓存行 + private func clearCacheRow(viewStore: ViewStoreOf) -> some View { + settingRow( + title: NSLocalizedString("appSetting.clearCache", comment: "Clear Cache"), + action: { viewStore.send(.clearCacheTapped) } + ) + } + + // MARK: - 检查更新行 + private func checkUpdatesRow(viewStore: ViewStoreOf) -> some View { + settingRow( + title: NSLocalizedString("appSetting.checkUpdates", comment: "Check for Updates"), + action: { viewStore.send(.checkUpdatesTapped) } + ) + } + + // MARK: - 关于我们行 + private func aboutUsRow(viewStore: ViewStoreOf) -> some View { + settingRow( + title: NSLocalizedString("appSetting.aboutUs", comment: "About Us"), + action: { viewStore.send(.aboutUsTapped) } + ) + } + + // MARK: - 设置项行 + private func settingRow(title: String, action: @escaping () -> Void) -> some View { + VStack(spacing: 0) { HStack { Text(title) .foregroundColor(.white) @@ -91,8 +214,45 @@ struct AppSettingView: View { } .padding(.horizontal, 32) .padding(.vertical, 18) + .onTapGesture { + action() + } + Divider().background(Color.gray.opacity(0.3)) .padding(.horizontal, 32) } } -} \ No newline at end of file + + // MARK: - 退出登录按钮 + private func logoutButton(viewStore: ViewStoreOf) -> some View { + Button(action: { + viewStore.send(.logoutTapped) + }) { + Text(NSLocalizedString("appSetting.logoutAccount", comment: "Log out of account")) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .background(Color.white.opacity(0.08)) + .cornerRadius(28) + .padding(.horizontal, 32) + } + .padding(.bottom, 32) + } + + // MARK: - 用户协议绑定 + private func userAgreementBinding(viewStore: ViewStoreOf) -> Binding { + viewStore.binding( + get: \.showUserAgreement, + send: AppSettingFeature.Action.userAgreementDismissed + ) + } + + // MARK: - 隐私政策绑定 + private func privacyPolicyBinding(viewStore: ViewStoreOf) -> Binding { + viewStore.binding( + get: \.showPrivacyPolicy, + send: AppSettingFeature.Action.privacyPolicyDismissed + ) + } +}