Files
chick c3d17b3daf release(ios): 0.2.2 构建 9 修复消息发送遮挡并中性化石墨灰阶
- 消息中心发送入口从被标签栏盖住的底部操作条移到导航栏右侧,发信恢复可用
- 停用态主按钮保留主色淡底与同色描边,与次级按钮拉开层级
- 消息行删除改安静线性图标,去掉一屏红块
- 缺数据读数统一为短横,卡片延迟补空格单位
- 中性灰阶改为严格 R=G=B,浅色主题不再读出冷调;图标石墨底同步去蓝
- 详情页延迟量表改用与卡片同一算法,身份信息值改为右对齐
- 标签加号按钮与输入框同高同圆角
2026-09-15 08:19:18 +08:00

158 lines
5.0 KiB
Swift

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))
}
}