import SwiftUI public struct AIButtonStyle: ButtonStyle { public static let pressedScale: CGFloat = 0.97 public enum Kind: Hashable { case primary case secondary case danger var foregroundColor: Color { foregroundColor(isEnabled: true) } var backgroundColor: Color { backgroundColor(isEnabled: true) } func foregroundColor(isEnabled: Bool) -> Color { switch self { case .primary: return isEnabled ? AITheme.onPrimary : AITheme.muted case .danger: return isEnabled ? AITheme.onDanger : AITheme.muted case .secondary: return isEnabled ? AITheme.ink : AITheme.muted } } func backgroundColor(isEnabled: Bool) -> Color { if isEnabled { return kindBackground } // 禁用态:实心主色按钮保留主色淡版,其余退回中性灰。 return self == .primary ? AITheme.primary.opacity(AITheme.disabledFillOpacity) : AITheme.control } private var kindBackground: Color { switch self { case .primary: return AITheme.primary case .secondary: return AITheme.control case .danger: return AITheme.danger } } /// 禁用态下主按钮必须还能看出"这是主行动"。 /// 之前三种 kind 禁用后都塌成同一块 control 灰,表单里"取消"和"保存"长得一模一样, /// 用户只能靠文字猜哪个是提交。这里让实心主色按钮改用同色相淡填充 + 描边,灰底次按钮不套。 var usesTintedDisabledTreatment: Bool { self == .primary } } public enum Size: Hashable { case compact case regular case large case icon case toolbar case toolbarInline } private let kind: Kind private let size: Size private let fixedWidthRequest: CGFloat? let fillsWidth: Bool @Environment(\.isEnabled) private var isEnabled public init( kind: Kind = .secondary, size: Size = .regular, fixedWidth: CGFloat? = nil, fillsWidth: Bool = false ) { self.kind = kind self.size = size self.fixedWidthRequest = fixedWidth self.fillsWidth = fillsWidth } public func makeBody(configuration: Configuration) -> some View { let height: CGFloat = switch size { case .compact: AITheme.compactButtonHeight case .large: AITheme.largeButtonHeight case .icon: AITheme.iconButtonSize case .regular: AITheme.buttonHeight case .toolbar: AITheme.toolbarButtonHeight case .toolbarInline: AITheme.toolbarButtonHeight } let isTintedDisabled = kind.usesTintedDisabledTreatment && !isEnabled configuration.label .labelStyle(.titleAndIcon) .font( .system(size: size == .compact || size == .toolbarInline ? 15 : 16, weight: .semibold) ) .lineLimit(1) .foregroundStyle(kind.foregroundColor(isEnabled: isEnabled)) .padding(.horizontal, fixedWidth == nil ? (kind == .secondary ? 12 : 14) : 0) .frame( minWidth: (!fillsWidth && fixedWidth == nil) ? minimumWidth : 0, minHeight: height ) .frame(maxWidth: fillsWidth ? .infinity : nil) .frame(width: fixedWidth) .background( // 主色面用纯色。之前的 primaryTop/primaryBottom 竖向渐变在系统里会被合成成灰紫, // 实测 #3B82F6 掉到 #6B81CA,整片按钮发脏。 AnyShapeStyle(kind.backgroundColor(isEnabled: isEnabled)) ) .shadow( color: (kind == .secondary || isTintedDisabled) ? .clear : .black.opacity(0.10), radius: 12, x: 0, y: 5 ) .clipShape( RoundedRectangle( cornerRadius: cornerRadius, style: .continuous ) ) .overlay { RoundedRectangle( cornerRadius: cornerRadius, style: .continuous ) .strokeBorder( isTintedDisabled ? AITheme.primary.opacity(AITheme.disabledStrokeOpacity) : size == .toolbarInline && kind == .primary ? AITheme.primary.opacity(0.28) : (kind == .secondary || !isEnabled ? AITheme.line : .clear) ) } .scaleEffect(configuration.isPressed ? Self.pressedScale : 1) .opacity(configuration.isPressed ? 0.82 : 1) } /// 工具栏里的图标按钮需要更小的圆角,否则 34pt 方块看起来像被切过。 private var cornerRadius: CGFloat { switch size { case .toolbarInline: return AITheme.toolbarButtonHeight / 2 case .icon: return AITheme.iconButtonRadius default: return AITheme.buttonRadius } } private var minimumWidth: CGFloat { switch size { case .compact: return AITheme.compactButtonMinWidth case .regular: return AITheme.regularButtonMinWidth case .large: return 92 case .icon: return AITheme.iconButtonSize case .toolbar, .toolbarInline: return AITheme.toolbarButtonMinWidth } } private var fixedWidth: CGFloat? { fixedWidthRequest ?? (size == .icon ? AITheme.iconButtonSize : (size == .toolbar ? AITheme.toolbarButtonWidth : nil)) } }