feat: 更新iOS和Podfile的部署目标以支持新版本
- 将iOS平台版本更新至17,确保与最新的开发环境兼容。 - 更新Podfile中的iOS部署目标至17.0,确保依赖项与新版本兼容。 - 修改Podfile.lock以反映新的依赖项版本,确保项目一致性。 - 在多个视图中重构代码,优化状态管理和视图逻辑,提升用户体验。
This commit is contained in:
@@ -5,7 +5,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "yana",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.iOS(.v17),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
4
Podfile
4
Podfile
@@ -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
|
||||
|
||||
|
@@ -27,6 +27,6 @@ SPEC CHECKSUMS:
|
||||
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
||||
|
||||
PODFILE CHECKSUM: cd339c4c75faaa076936a34cac2be597c64f138a
|
||||
PODFILE CHECKSUM: b6f9510b987dbfd80d7a7e45c13b229f9c4c6e63
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
@@ -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)";
|
||||
|
@@ -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: {
|
||||
// 强制关闭所有弹窗,放到action中,避免在视图更新周期set状态
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -5,37 +5,41 @@ import Combine
|
||||
struct EMailLoginView: View {
|
||||
let store: StoreOf<EMailLoginFeature>
|
||||
let onBack: () -> Void
|
||||
@Binding var showEmailLogin: Bool
|
||||
@Binding var showEmailLogin: Bool // 新增:绑定父视图的显示状态
|
||||
|
||||
// 使用本地@State管理UI状态
|
||||
@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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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: 登录成功,准备关闭自身")
|
||||
|
@@ -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 // 新增:邮箱登录导航状态
|
||||
|
||||
// 使用本地@State管理UI状态
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
@@ -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) }
|
||||
))
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user