feat: 更新iOS和Podfile的部署目标以支持新版本

- 将iOS平台版本更新至17,确保与最新的开发环境兼容。
- 更新Podfile中的iOS部署目标至17.0,确保依赖项与新版本兼容。
- 修改Podfile.lock以反映新的依赖项版本,确保项目一致性。
- 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。
This commit is contained in:
edwinQQQ
2025-07-29 15:59:09 +08:00
parent 567b1f3fd9
commit 3ec1b1302f
12 changed files with 1053 additions and 1131 deletions

View File

@@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "yana",
platforms: [
.iOS(.v15),
.iOS(.v17),
.macOS(.v12)
],
products: [

View File

@@ -1,5 +1,5 @@
# Uncomment the next line to define a global platform for your project
platform :ios, '16.0'
platform :ios, '17.0'
target 'yana' do
# Comment the next line if you don't want to use dynamic frameworks
@@ -26,7 +26,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
end
end

View File

@@ -27,6 +27,6 @@ SPEC CHECKSUMS:
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
PODFILE CHECKSUM: cd339c4c75faaa076936a34cac2be597c64f138a
PODFILE CHECKSUM: b6f9510b987dbfd80d7a7e45c13b229f9c4c6e63
COCOAPODS: 1.16.2

View File

@@ -49,6 +49,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = yanaAPITests;
sourceTree = "<group>";
};
@@ -253,14 +255,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-resources.sh\"\n";
@@ -274,14 +272,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";
@@ -391,7 +385,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.4;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@@ -451,7 +445,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.4;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
@@ -499,7 +493,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -557,7 +551,7 @@
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -588,7 +582,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = EKM7RAGNA6;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.junpeiqi.eparty;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -612,7 +606,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = EKM7RAGNA6;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stupidmonkey.yanaAPITests;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -31,476 +31,327 @@ struct AppSettingView: View {
@State private var errorMessage: String? = nil
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
mainView(viewStore: viewStore)
}
mainView()
}
}
@ViewBuilder
private func mainView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
contentWithImagePickers(viewStore: viewStore)
.onChange(of: selectedPhotoItems) { items in
print("[LOG] PhotosPicker选中items: \(items.count)")
guard !items.isEmpty else { return }
isLoading = true
selectedImages = []
let group = DispatchGroup()
var tempImages: [UIImage] = []
for item in items {
group.enter()
item.loadTransferable(type: Data.self) { result in
defer { group.leave() }
if let data = try? result.get(), let uiImage = UIImage(data: data) {
DispatchQueue.main.async {
tempImages.append(uiImage)
print("[LOG] 成功加载图片当前tempImages数量: \(tempImages.count)")
private func mainView() -> some View {
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
//
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
HStack {
Button(action: {
store.send(.dismissTapped)
}) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
} else {
print("[LOG] 图片加载失败")
Spacer()
Text(LocalizedString("app_settings.title", comment: "设置"))
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
Spacer()
//
Color.clear
.frame(width: 44, height: 44)
}
.padding(.horizontal, 16)
.padding(.top, 8)
//
ScrollView {
VStack(spacing: 20) {
//
avatarSection()
//
personalInfoSection()
//
otherSettingsSection()
// 退
logoutSection()
}
.padding(.horizontal, 20)
.padding(.top, 20)
.padding(.bottom, 40)
}
}
}
DispatchQueue.global().async {
group.wait()
DispatchQueue.main.async {
isLoading = false
print("[LOG] 所有图片加载完成tempImages数量: \(tempImages.count)")
if tempImages.isEmpty {
errorMessage = "图片加载失败,请重试"
//
showPreview = false
showCamera = false
showPhotoPicker = false
showActionSheet = false
print("[LOG] PhotosPicker图片加载失败弹出错误提示")
} else {
// selectedImages
selectedImages = tempImages
print("[LOG] selectedImages已设置数量: \(selectedImages.count)")
// 线showPreview
DispatchQueue.main.async {
showPreview = true
print("[LOG] showPreview已设置为true")
}
}
}
}
}
.alert(isPresented: Binding<Bool>(
get: { errorMessage != nil },
set: { if !$0 { errorMessage = nil } }
)) {
print("[LOG] 错误弹窗弹出: \(errorMessage ?? "")")
return Alert(title: Text(LocalizedString("app_settings.error", comment: "")), message: Text(errorMessage ?? ""), dismissButton: .default(Text(LocalizedString("app_settings.confirm", comment: "")), action: {
// actionset
showPreview = false
showCamera = false
showPhotoPicker = false
showActionSheet = false
}))
}
.navigationBarHidden(true)
.alert("修改昵称", isPresented: $showNicknameAlert) {
nicknameAlertContent(viewStore: viewStore)
} message: {
Text(LocalizedString("app_settings.nickname_limit", comment: ""))
.font(.caption)
.foregroundColor(.secondary)
//
.sheet(isPresented: $showImagePickerSheet) {
ImagePickerWithPreviewView(
store: pickerStore,
onUpload: { images in
if let firstImage = images.first,
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
store.send(AppSettingFeature.Action.avatarSelected(imageData))
}
.sheet(isPresented: userAgreementBinding(viewStore: viewStore)) {
WebView(url: APIConfiguration.webURL(for: .userAgreement)!)
}
.sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) {
WebView(url: APIConfiguration.webURL(for: .privacyPolicy)!)
}
.sheet(isPresented: deactivateAccountBinding(viewStore: viewStore)) {
WebView(url: APIConfiguration.webURL(for: .deactivateAccount)!)
}
}
@ViewBuilder
private func contentWithImagePickers(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack {
mainContent(viewStore: viewStore)
}
.confirmationDialog(
"请选择图片来源",
isPresented: $showActionSheet,
titleVisibility: .visible
) {
Button(LocalizedString("app_settings.take_photo", comment: "")) { showCamera = true }
Button(LocalizedString("app_settings.select_from_album", comment: "")) { showPhotoPicker = true }
Button("取消", role: .cancel) {}
}
.photosPicker(
isPresented: $showPhotoPicker,
selection: $selectedPhotoItems,
maxSelectionCount: 1,
matching: .images
)
.sheet(isPresented: $showCamera) {
CameraPicker { image in
print("[LOG] CameraPicker回调image: \(image != nil)")
if let image = image {
print("[LOG] CameraPicker获得图片直接上传头像")
if let data = image.jpegData(compressionQuality: 0.8) {
viewStore.send(AppSettingFeature.Action.avatarSelected(data))
}
} else {
errorMessage = "拍照失败,请重试"
//
showPreview = false
showCamera = false
showPhotoPicker = false
showActionSheet = false
print("[LOG] CameraPicker无图片弹出错误提示")
}
showCamera = false
}
}
.fullScreenCover(isPresented: $showPreview) {
ImagePreviewView(
images: $selectedImages,
currentIndex: .constant(0),
onConfirm: {
print("[LOG] 预览确认,准备上传头像")
if let image = selectedImages.first, let data = image.jpegData(compressionQuality: 0.8) {
viewStore.send(AppSettingFeature.Action.avatarSelected(data))
}
showPreview = false
showImagePickerSheet = false
},
onCancel: {
print("[LOG] 预览取消")
showPreview = false
showImagePickerSheet = false
}
)
}
//
.sheet(isPresented: $showCamera) {
ImagePickerWithPreviewView(
store: pickerStore,
onUpload: { images in
if let firstImage = images.first,
let imageData = firstImage.jpegData(compressionQuality: 0.8) {
store.send(AppSettingFeature.Action.avatarSelected(imageData))
}
// MARK: -
private func mainContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack {
Color(red: 22/255, green: 17/255, blue: 44/255).ignoresSafeArea()
VStack(spacing: 0) {
topBar
ScrollView {
WithPerceptionTracking {
VStack(spacing: 32) {
//
avatarSection(viewStore: viewStore)
//
nicknameSection(viewStore: viewStore)
//
settingsSection(viewStore: viewStore)
// 退
logoutButton(viewStore: viewStore)
showCamera = false
},
onCancel: {
showCamera = false
}
)
}
//
.alert(LocalizedString("app_settings.edit_nickname", comment: "编辑昵称"), isPresented: $showNicknameAlert) {
TextField(LocalizedString("app_settings.nickname_placeholder", comment: "请输入昵称"), text: $nicknameInput)
Button(LocalizedString("app_settings.cancel", comment: "取消")) {
showNicknameAlert = false
nicknameInput = ""
}
Button(LocalizedString("app_settings.confirm", comment: "确认")) {
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
store.send(.nicknameEditConfirmed(trimmed))
}
showNicknameAlert = false
nicknameInput = ""
}
} message: {
Text(LocalizedString("app_settings.nickname_tip", comment: "请输入新的昵称"))
}
}
}
// MARK: -
private func avatarSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
ZStack(alignment: .bottomTrailing) {
avatarImageView(viewStore: viewStore)
.onTapGesture {
showActionSheet = true
}
cameraButton(viewStore: viewStore)
}
.padding(.top, 24)
}
// MARK: -
// MARK: -
@ViewBuilder
private func avatarImageView(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
if viewStore.isUploadingAvatar || 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
private func avatarSection() -> some View {
WithPerceptionTracking {
VStack(spacing: 16) {
//
Button(action: {
showImagePickerSheet = true
}) {
AsyncImage(url: URL(string: store.userInfo?.avatar ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
defaultAvatarView
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundColor(.gray)
}
.frame(width: 120, height: 120)
.frame(width: 80, height: 80)
.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)
Circle()
.stroke(Color.white.opacity(0.3), lineWidth: 2)
)
}
// MARK: -
private func cameraButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
Button(action: {
showActionSheet = true
}) {
ZStack {
Circle().fill(Color.purple).frame(width: 36, height: 36)
Image(systemName: "camera.fill")
.foregroundColor(.white)
Text(LocalizedString("app_settings.tap_to_change_avatar", comment: "点击更换头像"))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
.padding(.vertical, 20)
.frame(maxWidth: .infinity)
.background(Color.white.opacity(0.1))
.cornerRadius(12)
}
.offset(x: 8, y: 8)
}
// MARK: -
private func nicknameSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
// MARK: -
@ViewBuilder
private func personalInfoSection() -> some View {
WithPerceptionTracking {
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 {
nicknameInput = viewStore.nickname
//
SettingRow(
icon: "person",
title: LocalizedString("app_settings.nickname", comment: "昵称"),
subtitle: store.userInfo?.nick ?? LocalizedString("app_settings.not_set", comment: "未设置"),
action: {
nicknameInput = store.userInfo?.nick ?? ""
showNicknameAlert = true
}
)
Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
// ID
SettingRow(
icon: "number",
title: LocalizedString("app_settings.user_id", comment: "用户ID"),
subtitle: "\(store.userInfo?.uid ?? 0)",
action: nil
)
}
.background(Color.white.opacity(0.1))
.cornerRadius(12)
}
}
// MARK: -
private func settingsSection(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
// MARK: -
@ViewBuilder
private func otherSettingsSection() -> some View {
WithPerceptionTracking {
VStack(spacing: 0) {
personalInfoPermissionsRow(viewStore: viewStore)
helpRow(viewStore: viewStore)
clearCacheRow(viewStore: viewStore)
checkUpdatesRow(viewStore: viewStore)
deactivateAccountRow(viewStore: viewStore)
aboutUsRow(viewStore: viewStore)
}
.background(Color.clear)
.padding(.horizontal, 0)
}
SettingRow(
icon: "hand.raised",
title: LocalizedString("app_settings.personal_info_permissions", comment: "个人信息权限"),
subtitle: LocalizedString("app_settings.manage_permissions", comment: "管理权限"),
action: { store.send(.personalInfoPermissionsTapped) }
)
// MARK: -
private func personalInfoPermissionsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.personalInfoPermissions", comment: "Personal Information and Permissions"),
action: { viewStore.send(.personalInfoPermissionsTapped) }
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "questionmark.circle",
title: LocalizedString("app_settings.help", comment: "帮助"),
subtitle: LocalizedString("app_settings.get_help", comment: "获取帮助"),
action: { store.send(.helpTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "trash",
title: LocalizedString("app_settings.clear_cache", comment: "清除缓存"),
subtitle: LocalizedString("app_settings.free_up_space", comment: "释放空间"),
action: { store.send(.clearCacheTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "arrow.clockwise",
title: LocalizedString("app_settings.check_updates", comment: "检查更新"),
subtitle: LocalizedString("app_settings.latest_version", comment: "最新版本"),
action: { store.send(.checkUpdatesTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "person.crop.circle.badge.minus",
title: LocalizedString("app_settings.deactivate_account", comment: "注销账号"),
subtitle: LocalizedString("app_settings.permanent_deletion", comment: "永久删除"),
action: { store.send(.deactivateAccountTapped) }
)
Divider()
.background(Color.white.opacity(0.2))
.padding(.leading, 50)
SettingRow(
icon: "info.circle",
title: LocalizedString("app_settings.about_us", comment: "关于我们"),
subtitle: LocalizedString("app_settings.app_info", comment: "应用信息"),
action: { store.send(.aboutUsTapped) }
)
}
// MARK: -
private func helpRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.help", comment: "Help"),
action: { viewStore.send(.helpTapped) }
)
}
// MARK: -
private func clearCacheRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.clearCache", comment: "Clear Cache"),
action: { viewStore.send(.clearCacheTapped) }
)
}
// MARK: -
private func checkUpdatesRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.checkUpdates", comment: "Check for Updates"),
action: { viewStore.send(.checkUpdatesTapped) }
)
}
// MARK: -
private func deactivateAccountRow(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
settingRow(
title: NSLocalizedString("appSetting.deactivateAccount", comment: "Deactivate Account"),
action: { viewStore.send(.deactivateAccountTapped) }
)
}
// MARK: -
private func aboutUsRow(viewStore: ViewStoreOf<AppSettingFeature>) -> 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)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding(.horizontal, 32)
.padding(.vertical, 18)
.onTapGesture {
action()
}
Divider().background(Color.gray.opacity(0.3))
.padding(.horizontal, 32)
.background(Color.white.opacity(0.1))
.cornerRadius(12)
}
}
// MARK: - 退
private func logoutButton(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
// MARK: - 退
@ViewBuilder
private func logoutSection() -> some View {
WithPerceptionTracking {
Button(action: {
viewStore.send(.logoutTapped)
store.send(.logoutTapped)
}) {
Text(NSLocalizedString("appSetting.logoutAccount", comment: "Log out of account"))
.font(.system(size: 18, weight: .semibold))
Text(LocalizedString("app_settings.logout", comment: "退出登录"))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.background(Color.white.opacity(0.08))
.cornerRadius(28)
.padding(.horizontal, 32)
.padding(.vertical, 16)
.background(Color.red.opacity(0.8))
.cornerRadius(12)
}
}
}
.padding(.bottom, 32)
}
// MARK: -
private func userAgreementBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding(
get: \.showUserAgreement,
send: AppSettingFeature.Action.userAgreementDismissed
)
}
// MARK: -
struct SettingRow: View {
let icon: String
let title: String
let subtitle: String
let action: (() -> Void)?
// MARK: -
private func privacyPolicyBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding(
get: \.showPrivacyPolicy,
send: AppSettingFeature.Action.privacyPolicyDismissed
)
}
// MARK: -
private func deactivateAccountBinding(viewStore: ViewStoreOf<AppSettingFeature>) -> Binding<Bool> {
viewStore.binding(
get: \.showDeactivateAccount,
send: AppSettingFeature.Action.deactivateAccountDismissed
)
}
// MARK: - Alert
@ViewBuilder
private func nicknameAlertContent(viewStore: ViewStoreOf<AppSettingFeature>) -> some View {
TextField("请输入昵称", text: $nicknameInput)
.onChange(of: nicknameInput) { newValue in
if newValue.count > 15 {
nicknameInput = String(newValue.prefix(15))
}
}
Button(LocalizedString("app_settings.confirm", comment: "")) {
let trimmed = nicknameInput.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty && trimmed != viewStore.nickname {
viewStore.send(.nicknameEditConfirmed(trimmed))
}
}
Button("取消", role: .cancel) {}
}
// MARK: -
private var topBar: some View {
HStack {
WithViewStore(store, observe: { $0 }) { viewStore in
var body: some View {
Button(action: {
viewStore.send(.dismissTapped)
action?()
}) {
Image(systemName: "chevron.left")
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 18))
.foregroundColor(.white)
.font(.system(size: 20, weight: .medium))
}
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
Text(subtitle)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
Spacer()
Text(NSLocalizedString("appSetting.title", comment: "Settings"))
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
Spacer()
//
Color.clear
.frame(width: 20, height: 20)
}
.padding(.horizontal, 20)
.padding(.top, 8)
.padding(.bottom, 16)
}
// MARK: -
private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping @Sendable (Data?) -> Void) {
item.loadTransferable(type: Data.self) { result in
guard let data = try? result.get(), let uiImage = UIImage(data: data) else {
completion(nil)
return
}
let square = cropToSquare(image: uiImage)
let resized = resizeImage(image: square, targetSize: CGSize(width: 180, height: 180))
let jpegData = resized.jpegData(compressionQuality: 0.8)
completion(jpegData)
if action != nil {
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.5))
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
// MARK: -
private func cropToSquare(image: UIImage) -> UIImage {
let size = min(image.size.width, image.size.height)
let x = (image.size.width - size) / 2
let y = (image.size.height - size) / 2
let cropRect = CGRect(x: x, y: y, width: size, height: size)
guard let cgImage = image.cgImage?.cropping(to: cropRect) else { return image }
return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation)
}
private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
.disabled(action == nil)
}
}

View File

@@ -5,37 +5,41 @@ import Combine
struct EMailLoginView: View {
let store: StoreOf<EMailLoginFeature>
let onBack: () -> Void
@Binding var showEmailLogin: Bool
@Binding var showEmailLogin: Bool //
// 使@StateUI
@State private var email: String = ""
@State private var verificationCode: String = ""
@State private var codeCountdown: Int = 0
@State private var timerCancellable: AnyCancellable?
@FocusState private var focusedField: Field?
//
@State private var timerCancellable: AnyCancellable?
//
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
}
private var getCodeButtonText: String {
if codeCountdown > 0 {
return "\(codeCountdown)s"
} else {
return LocalizedString("email_login.get_code", comment: "")
}
}
private var isCodeButtonEnabled: Bool {
return !store.isCodeLoading && codeCountdown == 0 && !email.isEmpty
}
enum Field {
case email
case verificationCode
}
private var isLoginButtonEnabled: Bool {
return !store.isLoading && !email.isEmpty && !verificationCode.isEmpty
}
private var getCodeButtonText: String {
if store.isCodeLoading {
return ""
} else if codeCountdown > 0 {
return "\(codeCountdown)S"
} else {
return LocalizedString("email_login.get_code", comment: "")
}
}
private var isCodeButtonEnabled: Bool {
return !store.isCodeLoading && codeCountdown == 0
}
var body: some View {
WithViewStore(store, observe: { $0.loginStep }) { viewStore in
WithPerceptionTracking {
LoginContentView(
store: store,
onBack: onBack,
@@ -47,7 +51,7 @@ struct EMailLoginView: View {
getCodeButtonText: getCodeButtonText,
isCodeButtonEnabled: isCodeButtonEnabled
)
.onChange(of: viewStore.state) { newStep in
.onChange(of: store.loginStep) { newStep in
debugInfoSync("🔄 EMailLoginView: loginStep 变化为 \(newStep)")
if newStep == .completed {
debugInfoSync("✅ EMailLoginView: 登录成功,准备关闭自身")
@@ -151,100 +155,94 @@ private struct LoginContentView: View {
.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(NSLocalizedString("placeholder.enter_email", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
VStack(spacing: 20) {
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Image("email icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(LocalizedString("email_login.email", comment: ""))
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.emailAddress)
.focused($focusedField, equals: .email)
.foregroundColor(.white)
}
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)
.textFieldStyle(PlainTextFieldStyle())
.font(.system(size: 16))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.focused($focusedField, equals: .email)
.keyboardType(.emailAddress)
.autocapitalization(.none)
}
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Image("id icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(LocalizedString("email_login.verification_code", comment: ""))
.font(.system(size: 16))
.foregroundColor(.white)
}
HStack {
TextField("", text: $verificationCode)
.placeholder(when: verificationCode.isEmpty) {
Text(NSLocalizedString("placeholder.enter_code", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.textFieldStyle(PlainTextFieldStyle())
.font(.system(size: 16))
.keyboardType(.numberPad)
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.focused($focusedField, equals: .verificationCode)
.keyboardType(.numberPad)
Button(action: {
store.send(.getVerificationCodeTapped)
store.send(.getVerificationCodeButtonTapped)
}) {
ZStack {
if store.isCodeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.7)
} else {
Text(getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(isCodeButtonEnabled ? Color.blue : Color.gray)
.cornerRadius(8)
}
.disabled(!isCodeButtonEnabled)
}
}
.frame(width: 60, height: 36)
.background(
RoundedRectangle(cornerRadius: 18)
.fill(Color.white.opacity(isCodeButtonEnabled && !email.isEmpty ? 0.2 : 0.1))
)
}
.disabled(!isCodeButtonEnabled || email.isEmpty || store.isCodeLoading)
}
.padding(.horizontal, 24)
}
}
.padding(.horizontal, 32)
Spacer().frame(height: 60)
//
Button(action: {
store.send(.loginButtonTapped(email: email, verificationCode: verificationCode))
store.send(.loginButtonTapped)
}) {
ZStack {
LinearGradient(
colors: [
Color(red: 0.85, green: 0.37, blue: 1.0),
Color(red: 0.54, green: 0.31, blue: 1.0)
],
startPoint: .leading,
endPoint: .trailing
)
.clipShape(RoundedRectangle(cornerRadius: 28))
HStack {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.8)
}
Text(store.isLoading ? LocalizedString("email_login.logging_in", comment: "") : LocalizedString("email_login.login_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.scaleEffect(1.2)
} else {
Text(LocalizedString("email_login.login", comment: ""))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || email.isEmpty || verificationCode.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5)
.padding(.horizontal, 32)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(isLoginButtonEnabled ? Color.blue : Color.gray)
.cornerRadius(8)
.disabled(!isLoginButtonEnabled)
.padding(.top, 20)
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
@@ -252,6 +250,7 @@ private struct LoginContentView: View {
.padding(.top, 16)
.padding(.horizontal, 32)
}
Spacer()
}
@@ -261,6 +260,8 @@ private struct LoginContentView: View {
}
}
}
.navigationBarHidden(true)
}
}
//#Preview {

View File

@@ -14,14 +14,14 @@ struct EditFeedView: View {
}
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
backgroundView
mainContent(geometry: geometry, viewStore: viewStore)
if viewStore.isUploadingImages {
uploadingImagesOverlay(progress: viewStore.imageUploadProgress, viewStore: viewStore)
} else if viewStore.isLoading {
mainContent(geometry: geometry)
if store.isUploadingImages {
uploadingImagesOverlay(progress: store.imageUploadProgress)
} else if store.isLoading {
loadingOverlay
}
}
@@ -41,181 +41,272 @@ struct EditFeedView: View {
isKeyboardVisible = false
}
}
.onChange(of: viewStore.errorMessage) { error in
if error != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
viewStore.send(.clearError)
}
.navigationBarHidden(true)
.onAppear {
store.send(.clearError)
}
}
.onChange(of: viewStore.shouldDismiss) { shouldDismiss in
.onChange(of: store.shouldDismiss) { shouldDismiss in
if shouldDismiss {
onDismiss()
NotificationCenter.default.post(name: Notification.Name("reloadFeedList"), object: nil)
viewStore.send(.clearDismissFlag)
}
}
.photosPicker(
isPresented: store.binding(
get: \.showPhotosPicker,
send: { _ in .photosPickerDismissed }
),
selection: store.binding(
get: \.selectedPhotoItems,
send: { .photosPickerItemsChanged($0) }
),
maxSelectionCount: 9,
matching: .images
)
.onChange(of: store.selectedPhotoItems) { items in
store.send(.photosPickerItemsChanged(items))
}
.onChange(of: store.selectedImages) { images in
//
}
.onChange(of: store.content) { content in
//
}
.alert("删除图片", isPresented: store.binding(
get: \.showDeleteImageAlert,
send: { _ in .deleteImageAlertDismissed }
)) {
Button("删除", role: .destructive) {
if let indexToDelete = store.imageToDeleteIndex {
store.send(.removeImage(indexToDelete))
}
}
Button("取消", role: .cancel) {
store.send(.deleteImageAlertDismissed)
}
} message: {
Text("确定要删除这张图片吗?")
}
}
}
private var backgroundView: some View {
Color(hexString: "0C0527")
Color(hex: 0x0C0527)
.ignoresSafeArea()
}
private func mainContent(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
private func mainContent(geometry: GeometryProxy) -> some View {
WithPerceptionTracking {
VStack(spacing: 0) {
headerView(geometry: geometry, viewStore: viewStore)
textInputArea(viewStore: viewStore)
//
ModernImageSelectionGrid(
images: viewStore.processedImages,
selectedItems: viewStore.selectedImages,
canAddMore: viewStore.canAddMoreImages,
onItemsChanged: { items in
viewStore.send(.photosPickerItemsChanged(items))
},
onRemoveImage: { index in
viewStore.send(.removeImage(index))
//
topNavigationBar
//
ScrollView {
VStack(spacing: 20) {
//
textInputSection
//
imageSelectionSection
//
publishButton
}
.padding(.horizontal, 20)
.padding(.bottom, 40)
}
)
.padding(.horizontal, 24)
.padding(.bottom, 32)
Spacer()
if !isKeyboardVisible {
publishButtonBottom(viewStore: viewStore, geometry: geometry)
}
}
}
private func headerView(geometry: GeometryProxy, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
private var topNavigationBar: some View {
WithPerceptionTracking {
HStack {
Text(LocalizedString("editFeed.title", comment: "Image & Text Edit"))
.font(.system(size: 20, weight: .semibold))
.foregroundColor(.white)
Spacer()
if isKeyboardVisible {
Button(action: {
hideKeyboard()
viewStore.send(.publishButtonTapped)
store.send(.clearDismissFlag)
onDismiss()
}) {
Text(LocalizedString("editFeed.publish", comment: "Publish"))
.font(.system(size: 16, weight: .semibold))
Image(systemName: "xmark")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
Spacer()
Text("编辑动态")
.font(.system(size: 18, weight: .medium))
.foregroundColor(.white)
Spacer()
//
Color.clear
.frame(width: 44, height: 44)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(
LinearGradient(
colors: [
Color(hexString: "A14AC6"),
Color(hexString: "3B1EEB")
],
startPoint: .leading,
endPoint: .trailing
)
.cornerRadius(16)
)
.padding(.top, 8)
.padding(.bottom, 16)
}
.disabled(!viewStore.canPublish)
}
}
.padding(.horizontal, 24)
.padding(.top, geometry.safeAreaInsets.top + 16)
.padding(.bottom, 24)
}
private func textInputArea(viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 20)
.fill(Color(hexString: "1C143A"))
TextEditor(text: Binding(
get: { viewStore.content },
set: { viewStore.send(.contentChanged($0)) }
))
.scrollContentBackground(.hidden)
.padding(16)
.frame(height: 160)
private var textInputSection: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 12) {
Text("分享你的想法...")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
.background(.clear)
.cornerRadius(20)
TextEditor(text: store.binding(
get: \.content,
send: { .contentChanged($0) }
))
.font(.system(size: 16))
if viewStore.content.isEmpty {
Text(LocalizedString("editFeed.enterContent", comment: "Enter Content"))
.foregroundColor(Color.white.opacity(0.4))
.padding(20)
.font(.system(size: 16))
}
VStack {
Spacer()
.foregroundColor(.white)
.background(Color.clear)
.frame(minHeight: 120)
.padding(12)
.background(Color.white.opacity(0.1))
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
HStack {
Spacer()
Text("\(viewStore.content.count)/\(maxCount)")
.foregroundColor(Color.white.opacity(0.4))
.font(.system(size: 14))
.padding(.trailing, 16)
.padding(.bottom, 10)
Text("\(store.content.count)/\(maxCount)")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
}
}
.frame(height: 160)
.padding(.horizontal, 24)
.padding(.bottom, 32)
}
private func publishButtonBottom(viewStore: ViewStoreOf<EditFeedFeature>, geometry: GeometryProxy) -> some View {
VStack {
Spacer()
Button(action: {
hideKeyboard()
viewStore.send(.publishButtonTapped)
}) {
Text(LocalizedString("editFeed.publish", comment: "Publish"))
.font(.system(size: 18, weight: .semibold))
private var imageSelectionSection: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 12) {
Text("添加图片")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
//
ForEach(Array(store.selectedImages.enumerated()), id: \.offset) { index, image in
imageItem(image: image, index: index)
}
//
if store.selectedImages.count < 9 {
addImageButton
}
}
}
}
}
private func imageItem(image: UIImage, index: Int) -> some View {
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 100)
.clipped()
.cornerRadius(8)
Button(action: {
store.send(.showDeleteImageAlert(index))
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 20))
.foregroundColor(.white)
.background(Color.black.opacity(0.5))
.clipShape(Circle())
}
.padding(4)
}
}
private var addImageButton: some View {
Button(action: {
store.send(.addImageButtonTapped)
}) {
VStack {
Image(systemName: "plus")
.font(.system(size: 24))
.foregroundColor(.white.opacity(0.7))
Text("添加")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.7))
}
.frame(height: 100)
.frame(maxWidth: .infinity)
.frame(height: 56)
.background(
LinearGradient(
colors: [Color(hexString: "A14AC6"), Color(hexString: "3B1EEB")],
startPoint: .leading,
endPoint: .trailing
)
.cornerRadius(28)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
}
.padding(.horizontal, 24)
.padding(.bottom, geometry.safeAreaInsets.bottom + 24)
.disabled(!viewStore.canPublish || viewStore.isUploadingImages || viewStore.isLoading)
.opacity(viewStore.canPublish ? 1.0 : 0.5)
}
private var publishButton: some View {
WithPerceptionTracking {
Button(action: {
store.send(.publishButtonTapped)
}) {
if store.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
} else {
Text("发布")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(store.content.isEmpty ? Color.gray : Color.blue)
.cornerRadius(12)
.disabled(store.isLoading || store.content.isEmpty)
}
}
private func uploadingImagesOverlay(progress: Double) -> some View {
WithPerceptionTracking {
ZStack {
Color.black.opacity(0.5)
.ignoresSafeArea()
VStack(spacing: 16) {
ProgressView(value: progress)
.progressViewStyle(LinearProgressViewStyle(tint: .white))
.frame(width: 200)
Text("上传图片中... \(Int(progress * 100))%")
.font(.system(size: 16))
.foregroundColor(.white)
}
.padding(24)
.background(Color.black.opacity(0.8))
.cornerRadius(12)
}
}
}
private var loadingOverlay: some View {
Group {
Color.black.opacity(0.3)
WithPerceptionTracking {
ZStack {
Color.black.opacity(0.5)
.ignoresSafeArea()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.5)
}
}
//
private func uploadingImagesOverlay(progress: Double, viewStore: ViewStoreOf<EditFeedFeature>) -> some View {
Group {
Color.black.opacity(0.3)
.ignoresSafeArea()
VStack {
ProgressView()
.scaleEffect(1.2)
Text(String(format: LocalizedString("edit_feed.uploading_progress", comment: ""), Int(progress * 100)))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
.padding(.top, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}

View File

@@ -172,35 +172,35 @@ struct FeedListContentView: View {
@Binding var previewCurrentIndex: Int
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
if viewStore.isLoading {
WithPerceptionTracking {
if store.isLoading {
LoadingView()
} else if let error = viewStore.error {
} else if let error = store.error {
ErrorView(error: error)
} else if viewStore.moments.isEmpty {
} else if store.moments.isEmpty {
EmptyView()
} else {
MomentsListView(
moments: viewStore.moments,
hasMore: viewStore.hasMore,
isLoadingMore: viewStore.isLoadingMore,
moments: store.moments,
hasMore: store.hasMore,
isLoadingMore: store.isLoadingMore,
onImageTap: { images, tappedIndex in
previewCurrentIndex = tappedIndex
previewItem = PreviewItem(images: images, index: tappedIndex)
},
onMomentTap: { moment in
viewStore.send(.showDetail(moment))
store.send(.showDetail(moment))
},
onLikeTap: { dynamicId, uid, likedUid, worldId in
viewStore.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
store.send(.likeDynamic(dynamicId, uid, likedUid, worldId))
},
onLoadMore: {
viewStore.send(.loadMore)
store.send(.loadMore)
},
onRefresh: {
viewStore.send(.reload)
store.send(.reload)
},
likeLoadingDynamicIds: viewStore.likeLoadingDynamicIds
likeLoadingDynamicIds: store.likeLoadingDynamicIds
)
}
}
@@ -214,7 +214,6 @@ struct FeedListView: View {
@State private var previewCurrentIndex: Int = 0
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
@@ -224,7 +223,7 @@ struct FeedListView: View {
VStack(alignment: .center, spacing: 0) {
//
TopBarView {
viewStore.send(.editFeedButtonTapped)
store.send(.editFeedButtonTapped)
}
//
@@ -248,25 +247,23 @@ struct FeedListView: View {
Spacer()
}
.frame(maxWidth: .infinity, alignment: .top)
}
// API Loading
APILoadingEffectView()
}
.onAppear {
viewStore.send(.onAppear)
store.send(.onAppear)
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("reloadFeedList"))) { _ in
viewStore.send(.reload)
.onRefresh {
store.send(.reload)
}
.sheet(isPresented: viewStore.binding(
get: \.isEditFeedPresented,
send: { $0 ? .editFeedButtonTapped : .editFeedDismissed }
//
.sheet(isPresented: store.binding(
get: \.showEditFeed,
send: { _ in .editFeedDismissed }
)) {
WithPerceptionTracking {
EditFeedView(
onDismiss: {
viewStore.send(.editFeedDismissed)
store.send(.editFeedDismissed)
},
store: Store(
initialState: EditFeedFeature.State()
@@ -275,21 +272,13 @@ struct FeedListView: View {
}
)
}
//
.fullScreenCover(item: $previewItem) { item in
ImagePreviewPager(
images: item.images as [String],
currentIndex: $previewCurrentIndex
) {
previewItem = nil
}
}
// DetailView
.navigationDestination(isPresented: viewStore.binding(
//
.navigationDestination(isPresented: store.binding(
get: \.showDetail,
send: { _ in .detailDismissed }
)) {
if let selectedMoment = viewStore.selectedMoment {
if let selectedMoment = store.selectedMoment {
DetailView(
store: Store(
initialState: DetailFeature.State(moment: selectedMoment)
@@ -297,11 +286,16 @@ struct FeedListView: View {
DetailFeature()
},
onDismiss: {
viewStore.send(.detailDismissed)
store.send(.detailDismissed)
}
)
}
}
//
.fullScreenCover(item: $previewItem) { item in
ImagePreviewPager(images: item.images as [String], currentIndex: $previewCurrentIndex) {
previewItem = nil
}
}
}
}

View File

@@ -21,9 +21,8 @@ struct IDLoginView: View {
}
var body: some View {
WithViewStore(store, observe: { $0.loginStep }) { viewStore in
GeometryReader { geometry in
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
// - 使"bg"
Image("bg")
@@ -58,67 +57,70 @@ struct IDLoginView: View {
.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(LocalizedString("placeholder.enter_id", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
VStack(spacing: 20) {
// ID
VStack(alignment: .leading, spacing: 8) {
HStack {
Image("id icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(LocalizedString("id_login.user_id", comment: ""))
.font(.system(size: 16))
.padding(.horizontal, 24)
.keyboardType(.numberPad)
.foregroundColor(.white)
}
TextField("", text: $userID)
.textFieldStyle(PlainTextFieldStyle())
.font(.system(size: 16))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.onChange(of: userID) { newValue in
store.send(.userIDChanged(newValue))
}
}
//
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)
VStack(alignment: .leading, spacing: 8) {
HStack {
Image("email icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text(LocalizedString("id_login.password", comment: ""))
.font(.system(size: 16))
.foregroundColor(.white)
}
HStack {
if isPasswordVisible {
TextField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text(LocalizedString("placeholder.enter_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
TextField("", text: $password)
.textFieldStyle(PlainTextFieldStyle())
} else {
SecureField("", text: $password) // 使SwiftUI
.placeholder(when: password.isEmpty) {
Text(LocalizedString("placeholder.enter_password", comment: ""))
.foregroundColor(.white.opacity(0.6))
}
.foregroundColor(.white)
.font(.system(size: 16))
SecureField("", text: $password)
.textFieldStyle(PlainTextFieldStyle())
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
.foregroundColor(.white.opacity(0.6))
Image(systemName: isPasswordVisible ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
}
}
.font(.system(size: 16))
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.onChange(of: password) { newValue in
store.send(.passwordChanged(newValue))
}
}
.padding(.horizontal, 24)
}
//
HStack {
@@ -131,43 +133,29 @@ struct IDLoginView: View {
.foregroundColor(.white.opacity(0.8))
}
}
.padding(.horizontal, 8)
//
Button(action: {
// action
store.send(.loginButtonTapped(userID: userID, password: password))
store.send(.loginButtonTapped)
}) {
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 ? LocalizedString("id_login.logging_in", comment: "") : LocalizedString("id_login.login_button", comment: ""))
.font(.system(size: 18, weight: .semibold))
.scaleEffect(1.2)
} else {
Text(LocalizedString("id_login.login", comment: ""))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
}
.frame(height: 56)
}
.disabled(store.isLoading || userID.isEmpty || password.isEmpty)
.opacity(isLoginButtonEnabled ? 1.0 : 0.5) // 50%
.padding(.horizontal, 32)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(isLoginButtonEnabled ? Color.blue : Color.gray)
.cornerRadius(8)
.disabled(!isLoginButtonEnabled)
.padding(.top, 20)
//
//
if let errorMessage = store.errorMessage {
Text(errorMessage)
.font(.system(size: 14))
@@ -184,7 +172,6 @@ struct IDLoginView: View {
}
}
}
}
.navigationBarHidden(true)
// 使 LoginView navigationDestination
.navigationDestination(isPresented: $showRecoverPassword) {
@@ -215,8 +202,7 @@ struct IDLoginView: View {
#endif
}
}
//
.onChange(of: viewStore.state) { newStep in
.onChange(of: store.loginStep) { newStep in
debugInfoSync("🔄 IDLoginView: loginStep 变化为 \(newStep)")
if newStep == .completed {
debugInfoSync("✅ IDLoginView: 登录成功,准备关闭自身")

View File

@@ -13,20 +13,21 @@ struct ImageHeightPreferenceKey: PreferenceKey {
struct LoginView: View {
let store: StoreOf<LoginFeature>
let onLoginSuccess: () -> Void //
@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
@State private var showEmailLogin = false //
// 使@StateUI
@State private var showIDLogin: Bool = false
@State private var showEmailLogin: Bool = false
@State private var showLanguageSettings: Bool = false
@State private var showUserAgreement: Bool = false
@State private var showPrivacyPolicy: Bool = false
//
private var topImageHeight: CGFloat = 200 //
var body: some View {
WithViewStore(self.store, observe: { $0.isAnyLoginCompleted }) { viewStore in
WithPerceptionTracking {
NavigationStack {
GeometryReader { geometry in
WithPerceptionTracking {
ZStack {
// 使 splash
Image("bg")
@@ -55,64 +56,54 @@ struct LoginView: View {
Spacer()
}
.padding(.top, max(0, topImageHeight - 100)) // top - 140
}
// - Debug
#if DEBUG
VStack {
HStack {
Spacer()
//
VStack(spacing: 20) {
//
LoginButton(
title: LocalizedString("login.id_login", comment: ""),
icon: "person.circle",
action: {
showIDLogin = true
}
)
LoginButton(
title: LocalizedString("login.email_login", comment: ""),
icon: "envelope",
action: {
showEmailLogin = true
}
)
//
HStack(spacing: 20) {
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: LocalizedString("login.id_login", comment: "")
) {
showIDLogin = true // SwiftUI
}
// Email Login
LoginButton(
iconName: "envelope.fill",
iconColor: .blue,
title: LocalizedString("login.email_login", comment: "")
) {
showEmailLogin = true //
}
}.padding(.top, max(0, topImageHeight+140))
}
.onPreferenceChange(ImageHeightPreferenceKey.self) { imageHeight in
topImageHeight = imageHeight
Text(LocalizedString("login.language", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
Spacer()
.frame(height: 120)
//
UserAgreementView(
isAgreed: $isAgreedToTerms,
onUserServiceTapped: {
Button(action: {
showUserAgreement = true
},
onPrivacyPolicyTapped: {
showPrivacyPolicy = true
}) {
Text(LocalizedString("login.user_agreement", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
Button(action: {
showPrivacyPolicy = true
}) {
Text(LocalizedString("login.privacy_policy", comment: ""))
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
}
}
}
)
.padding(.horizontal, 28)
.padding(.bottom, 140)
}
@@ -123,7 +114,6 @@ struct LoginView: View {
// NavigationLink navigationDestination
}
}
}
.navigationBarHidden(true)
// iOS 16 navigationDestination
.navigationDestination(isPresented: $showIDLogin) {
@@ -171,20 +161,20 @@ struct LoginView: View {
url: APIConfiguration.webURL(for: .privacyPolicy)
)
//
.onChange(of: viewStore.state) { completed in
.onChange(of: store.isAnyLoginCompleted) { completed in
if completed {
onLoginSuccess()
}
}
// showIDLogin
.onChange(of: showIDLogin) { newValue in
if newValue == false && viewStore.state {
if newValue == false && store.isAnyLoginCompleted {
onLoginSuccess()
}
}
// showEmailLogin
.onChange(of: showEmailLogin) { newValue in
if newValue == false && viewStore.state {
if newValue == false && store.isAnyLoginCompleted {
onLoginSuccess()
}
}

View File

@@ -6,10 +6,9 @@ struct MainView: View {
var onLogout: (() -> Void)? = nil
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
InternalMainView(store: store)
.onChange(of: viewStore.isLoggedOut) { isLoggedOut in
.onChange(of: store.isLoggedOut) { isLoggedOut in
if isLoggedOut {
onLogout?()
}
@@ -17,7 +16,6 @@ struct MainView: View {
}
}
}
}
struct InternalMainView: View {
let store: StoreOf<MainFeature>
@@ -27,25 +25,23 @@ struct InternalMainView: View {
_path = State(initialValue: store.withState { $0.navigationPath })
}
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
NavigationStack(path: $path) {
GeometryReader { geometry in
contentView(geometry: geometry, viewStore: viewStore)
contentView(geometry: geometry)
.navigationDestination(for: MainFeature.Destination.self) { destination in
DestinationView(destination: destination, store: self.store)
}
.onChange(of: path) { newPath in
viewStore.send(.navigationPathChanged(newPath))
store.send(.navigationPathChanged(newPath))
}
.onChange(of: viewStore.navigationPath) { newPath in
.onChange(of: store.navigationPath) { newPath in
if path != newPath {
path = newPath
}
}
.onAppear {
viewStore.send(.onAppear)
}
store.send(.onAppear)
}
}
}
@@ -74,7 +70,7 @@ struct InternalMainView: View {
}
}
private func contentView(geometry: GeometryProxy, viewStore: ViewStoreOf<MainFeature>) -> some View {
private func contentView(geometry: GeometryProxy) -> some View {
WithPerceptionTracking {
ZStack {
//
@@ -87,7 +83,7 @@ struct InternalMainView: View {
//
MainContentView(
store: store,
selectedTab: viewStore.selectedTab
selectedTab: store.selectedTab
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.bottom, 80) //
@@ -95,7 +91,7 @@ struct InternalMainView: View {
// -
VStack {
Spacer()
BottomTabView(selectedTab: viewStore.binding(
BottomTabView(selectedTab: store.binding(
get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed },
send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
))

View File

@@ -8,7 +8,6 @@ struct MeView: View {
@State private var previewCurrentIndex: Int = 0
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
GeometryReader { geometry in
ZStack {
@@ -25,7 +24,7 @@ struct MeView: View {
HStack {
Spacer()
Button(action: {
viewStore.send(.settingButtonTapped)
store.send(.settingButtonTapped)
}) {
Image(systemName: "gearshape")
.font(.system(size: 33, weight: .medium))
@@ -40,10 +39,10 @@ struct MeView: View {
//
VStack(spacing: 16) {
//
userInfoSection(viewStore: viewStore)
userInfoSection()
//
momentsSection(viewStore: viewStore)
momentsSection()
Spacer()
}
@@ -52,7 +51,7 @@ struct MeView: View {
}
}
.onAppear {
viewStore.send(.onAppear)
store.send(.onAppear)
}
//
.fullScreenCover(item: $previewItem) { item in
@@ -61,11 +60,11 @@ struct MeView: View {
}
}
//
.navigationDestination(isPresented: viewStore.binding(
.navigationDestination(isPresented: store.binding(
get: \.showDetail,
send: { _ in .detailDismissed }
)) {
if let selectedMoment = viewStore.selectedMoment {
if let selectedMoment = store.selectedMoment {
DetailView(
store: Store(
initialState: DetailFeature.State(moment: selectedMoment)
@@ -73,89 +72,108 @@ struct MeView: View {
DetailFeature()
},
onDismiss: {
viewStore.send(.detailDismissed)
store.send(.detailDismissed)
}
)
}
}
}
}
}
// MARK: -
@ViewBuilder
private func userInfoSection(viewStore: ViewStoreOf<MeFeature>) -> some View {
if viewStore.isLoadingUserInfo {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.frame(height: 130)
} else if let error = viewStore.userInfoError {
Text(error)
.font(.system(size: 14))
.foregroundColor(.red)
.frame(height: 130)
} else if let userInfo = viewStore.userInfo {
VStack(spacing: 8) {
if let avatarUrl = userInfo.avatar, !avatarUrl.isEmpty {
AsyncImage(url: URL(string: avatarUrl)) { image in
image.resizable().aspectRatio(contentMode: .fill).clipShape(Circle())
private func userInfoSection() -> some View {
WithPerceptionTracking {
VStack(spacing: 16) {
//
AsyncImage(url: URL(string: store.userInfo?.avatar ?? "")) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundColor(.gray)
}
.frame(width: 90, height: 90)
} else {
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
.frame(width: 90, height: 90)
}
Text(userInfo.nick ?? LocalizedString("me.nickname", comment: "用户昵称"))
.font(.system(size: 18, weight: .medium))
.frame(width: 80, height: 80)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.white.opacity(0.3), lineWidth: 2)
)
//
Text(store.userInfo?.nick ?? "未知用户")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
Text(LocalizedString("me.id", comment: "ID: %@").localized(arguments: String(userInfo.uid ?? 0)))
// ID
Text("ID: \(store.userInfo?.uid ?? 0)")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
.padding(.top, 0)
.frame(height: 130)
} else {
Spacer().frame(height: 130)
.padding(.horizontal, 32)
}
}
// MARK: -
@ViewBuilder
private func momentsSection(viewStore: ViewStoreOf<MeFeature>) -> some View {
if viewStore.isLoadingMoments && viewStore.moments.isEmpty {
ProgressView(LocalizedString("feed.loadingMore", comment: "加载中..."))
private func momentsSection() -> some View {
WithPerceptionTracking {
if store.isLoading {
VStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
Text("加载中...")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
.padding(.top, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = viewStore.momentsError {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 40))
.foregroundColor(.yellow)
} else if let error = store.error {
VStack(spacing: 12) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 32))
.foregroundColor(.orange)
Text("加载失败")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
Text(error)
.foregroundColor(.red)
Button(LocalizedString("feed.retry", comment: "重试")) {
viewStore.send(.onAppear)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
Button("重试") {
store.send(.onAppear)
}
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.8))
.cornerRadius(8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if viewStore.moments.isEmpty {
VStack(spacing: 16) {
Image(systemName: "tray")
.font(.system(size: 40))
.foregroundColor(.gray)
Text(LocalizedString("feed.empty", comment: "暂无动态"))
.foregroundColor(.white.opacity(0.8))
} else if store.moments.isEmpty {
VStack(spacing: 12) {
Image(systemName: "doc.text")
.font(.system(size: 32))
.foregroundColor(.white.opacity(0.5))
Text("暂无动态")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white.opacity(0.7))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollView {
WithPerceptionTracking {
LazyVStack(spacing: 12) {
ForEach(Array(viewStore.moments.enumerated()), id: \.element.dynamicId) { index, moment in
ForEach(Array(store.moments.enumerated()), id: \.element.dynamicId) { index, moment in
OptimizedDynamicCardView(
moment: moment,
allMoments: viewStore.moments,
allMoments: store.moments,
currentIndex: index,
onImageTap: { images, tappedIndex in
previewCurrentIndex = tappedIndex
@@ -165,15 +183,15 @@ struct MeView: View {
//
},
onCardTap: {
viewStore.send(.showDetail(moment))
store.send(.showDetail(moment))
}
)
.padding(.horizontal, 12)
}
if viewStore.hasMore {
if store.hasMore {
ProgressView()
.onAppear {
viewStore.send(.loadMore)
store.send(.loadMore)
}
}
//
@@ -183,7 +201,8 @@ struct MeView: View {
}
}
.refreshable {
viewStore.send(.refresh)
store.send(.refresh)
}
}
}
}