Files
e-party-iOS/yana/MVVM/CommonComponents.swift
edwinQQQ 90a840c5f3 feat: 更新日志级别管理及底部导航栏组件化
- 在ContentView中根据编译模式初始化日志级别,确保调试信息的灵活性。
- 在APILogger中使用actor封装日志级别,增强并发安全性。
- 新增通用底部Tab栏组件,优化MainPage中的底部导航逻辑,提升代码可维护性。
- 移除冗余的AppRootView和MainView,简化视图结构,提升代码整洁性。
2025-09-18 16:12:18 +08:00

372 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
var body: some View {
HStack(spacing: 0) {
ForEach(items) { item in
Button(action: {
selectedId = item.id
onSelect(item.id)
}) {
VStack(spacing: 4) {
Image(systemName: item.systemIconName)
.font(.system(size: 24))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
Text(item.title)
.font(.system(size: 12))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
}
}
.frame(maxWidth: .infinity)
.padding(contentPadding)
}
}
.padding(.horizontal, horizontalPadding)
.background(LiquidGlassBackground())
.clipShape(Capsule())
.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<String>
let isPasswordVisible: Binding<Bool>?
let onGetCode: (() -> Void)?
let isCodeButtonEnabled: Bool
let isCodeLoading: Bool
let getCodeButtonText: String
init(
type: InputFieldType,
placeholder: String,
text: Binding<String>,
isPasswordVisible: Binding<Bool>? = 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: {}
)
}
}