import SwiftUI // MARK: - App Image Source Enum enum AppImageSource: Equatable { case camera case photoLibrary } // MARK: - 通用底部 Tab 栏组件 public struct TabBarItem: Identifiable, Equatable { public let id: String public let title: String public let systemIconName: String public init(id: String, title: String, systemIconName: String) { self.id = id self.title = title self.systemIconName = systemIconName } } struct BottomTabBar: View { let items: [TabBarItem] @Binding var selectedId: String let onSelect: (String) -> Void var contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0) var horizontalPadding: CGFloat = 0 // 便捷初始化:内部固定 tabs,避免外部重复声明 init( selectedId: Binding, onSelect: @escaping (String) -> Void, contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0), horizontalPadding: CGFloat = 0 ) { self.items = BottomTabBar.defaultItems() self._selectedId = selectedId self.onSelect = onSelect self.contentPadding = contentPadding self.horizontalPadding = horizontalPadding } // 最简初始化:直接接受 viewModel,内部处理所有逻辑 init(viewModel: MainViewModel) { self.items = BottomTabBar.defaultItems() self._selectedId = Binding( get: { viewModel.selectedTab.rawValue }, set: { raw in if let tab = MainViewModel.Tab(rawValue: raw) { viewModel.onTabChanged(tab) } } ) self.onSelect = { _ in } // 保留但不再使用 self.contentPadding = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0) self.horizontalPadding = 0 } // 使用 BottomTabView.swift 中的图片资源名进行映射 private func assetIconName(for item: TabBarItem, isSelected: Bool) -> String? { switch item.id { case "feed": return isSelected ? "feed selected" : "feed unselected" case "me": return isSelected ? "me selected" : "me unselected" default: return nil } } // 内部默认 items(与资源映射保持一致) private static func defaultItems() -> [TabBarItem] { return [ TabBarItem(id: "feed", title: "Feed", systemIconName: "list.bullet"), TabBarItem(id: "me", title: "Me", systemIconName: "person.circle") ] } var body: some View { HStack(spacing: 8) { ForEach(items) { item in Button(action: { selectedId = item.id onSelect(item.id) }) { Group { if let name = assetIconName(for: item, isSelected: selectedId == item.id) { Image(name) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 30, height: 30) } else { Image(systemName: item.systemIconName) .font(.system(size: 24)) .foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6)) } } } .frame(maxWidth: .infinity) .padding(contentPadding) .contentShape(Rectangle()) } } .padding(.horizontal, 8) // 按钮与边缘保持 8 间距 .padding(.horizontal, horizontalPadding) .background(LiquidGlassBackground()) .clipShape(Capsule()) .contentShape(Capsule()) .onTapGesture { /* 吸收空白区域点击,避免穿透 */ } .overlay( Capsule() .stroke(Color.white.opacity(0.12), lineWidth: 0.5) ) .shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 6) .safeAreaInset(edge: .bottom) { Color.clear.frame(height: 0) } } } // MARK: - Liquid Glass Background (iOS 26 优先,向下优雅降级) struct LiquidGlassBackground: View { var body: some View { Group { if #available(iOS 26.0, *) { // iOS 26+:使用系统液态玻璃效果 Rectangle() .fill(Color.clear) .glassEffect() } else if #available(iOS 17.0, *) { // iOS 17-25:使用超薄材质 + 轻微高光层 ZStack { Rectangle().fill(.ultraThinMaterial) LinearGradient( colors: [Color.white.opacity(0.06), Color.clear, Color.white.opacity(0.04)], startPoint: .topLeading, endPoint: .bottomTrailing ) .blendMode(.softLight) } } else { // 更低版本:半透明备选 Rectangle() .fill(Color.black.opacity(0.2)) } } } } // MARK: - 背景视图组件 struct LoginBackgroundView: View { var body: some View { Image("bg") .resizable() .aspectRatio(contentMode: .fill) // .ignoresSafeArea(.all) } } // MARK: - 顶部导航栏组件 struct LoginHeaderView: View { let onBack: () -> Void var body: some View { HStack { Button(action: onBack) { Image(systemName: "chevron.left") .font(.system(size: 24, weight: .medium)) .foregroundColor(.white) .frame(width: 44, height: 44) } Spacer() } .padding(.horizontal, 16) .padding(.top, 8) } } // MARK: - 通用输入框组件 enum InputFieldType { case text case number case password case verificationCode } struct CustomInputField: View { let type: InputFieldType let placeholder: String let text: Binding let isPasswordVisible: Binding? let onGetCode: (() -> Void)? let isCodeButtonEnabled: Bool let isCodeLoading: Bool let getCodeButtonText: String init( type: InputFieldType, placeholder: String, text: Binding, isPasswordVisible: Binding? = nil, onGetCode: (() -> Void)? = nil, isCodeButtonEnabled: Bool = false, isCodeLoading: Bool = false, getCodeButtonText: String = "" ) { self.type = type self.placeholder = placeholder self.text = text self.isPasswordVisible = isPasswordVisible self.onGetCode = onGetCode self.isCodeButtonEnabled = isCodeButtonEnabled self.isCodeLoading = isCodeLoading self.getCodeButtonText = getCodeButtonText } var body: some View { ZStack { RoundedRectangle(cornerRadius: 25) .fill(Color.white.opacity(0.1)) .overlay( RoundedRectangle(cornerRadius: 25) .stroke(Color.white.opacity(0.3), lineWidth: 1) ) .frame(height: 56) HStack { // 输入框 Group { switch type { case .text, .number: TextField("", text: text) .placeholder(when: text.wrappedValue.isEmpty) { Text(placeholder) .foregroundColor(.white.opacity(0.6)) } .keyboardType(type == .number ? .numberPad : .default) case .password: if let isPasswordVisible = isPasswordVisible { if isPasswordVisible.wrappedValue { TextField("", text: text) .placeholder(when: text.wrappedValue.isEmpty) { Text(placeholder) .foregroundColor(.white.opacity(0.6)) } } else { SecureField("", text: text) .placeholder(when: text.wrappedValue.isEmpty) { Text(placeholder) .foregroundColor(.white.opacity(0.6)) } } } case .verificationCode: TextField("", text: text) .placeholder(when: text.wrappedValue.isEmpty) { Text(placeholder) .foregroundColor(.white.opacity(0.6)) } .keyboardType(.numberPad) } } .foregroundColor(.white) .font(.system(size: 16)) // 右侧按钮 if type == .password, let isPasswordVisible = isPasswordVisible { Button(action: { isPasswordVisible.wrappedValue.toggle() }) { Image(systemName: isPasswordVisible.wrappedValue ? "eye.slash" : "eye") .foregroundColor(.white.opacity(0.7)) .font(.system(size: 18)) } } else if type == .verificationCode, let onGetCode = onGetCode { Button(action: onGetCode) { ZStack { if isCodeLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(0.7) } else { Text(getCodeButtonText) .font(.system(size: 14, weight: .medium)) .foregroundColor(.white) } } .frame(width: 60, height: 36) .background( RoundedRectangle(cornerRadius: 15) .fill(Color.white.opacity(isCodeButtonEnabled ? 0.2 : 0.1)) ) } .disabled(!isCodeButtonEnabled || isCodeLoading) } } .padding(.horizontal, 24) } } } // MARK: - 登录按钮组件 struct LoginButtonView: View { let isLoading: Bool let isEnabled: Bool let onTap: () -> Void var body: some View { Button(action: onTap) { Group { if isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .scaleEffect(1.2) } else { Text("Login") .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) } } } .frame(maxWidth: .infinity) .padding(.vertical, 16) .background(isEnabled ? Color(red: 0.5, green: 0.3, blue: 0.8) : Color.gray) .cornerRadius(8) .disabled(!isEnabled) } } // MARK: - 设置行组件 struct SettingRow: View { let title: String let subtitle: String let action: (() -> Void)? var body: some View { Button(action: { action?() }) { HStack(spacing: 16) { HStack { Text(title) .font(.system(size: 16)) .foregroundColor(.white) .multilineTextAlignment(.leading) Spacer() if !subtitle.isEmpty { Text(subtitle) .font(.system(size: 14)) .foregroundColor(.white.opacity(0.7)) } } Spacer() if action != nil { Image(systemName: "chevron.right") .font(.system(size: 14)) .foregroundColor(.white.opacity(0.5)) } } .padding(.horizontal, 16) .padding(.vertical, 12) } .disabled(action == nil) } } // MARK: - Camera Picker struct CameraPicker: UIViewControllerRepresentable { let onImagePicked: (UIImage?) -> Void func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = .camera picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(onImagePicked: onImagePicked) } class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { let onImagePicked: (UIImage?) -> Void init(onImagePicked: @escaping (UIImage?) -> Void) { self.onImagePicked = onImagePicked } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let image = info[.originalImage] as? UIImage { onImagePicked(image) } else { onImagePicked(nil) } picker.dismiss(animated: true) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { onImagePicked(nil) picker.dismiss(animated: true) } } } #Preview { VStack(spacing: 20) { LoginBackgroundView() LoginHeaderView(onBack: {}) CustomInputField( type: .text, placeholder: "Test Input", text: .constant("") ) LoginButtonView( isLoading: false, isEnabled: true, onTap: {} ) } }