import SwiftUI // MARK: - 通用计量器:根据 GaugeDisplayStyle 动态渲染 struct StyledGauge: View { let title: String let icon: String let value: Double let unit: String let detail: String let style: GaugeDisplayStyle let fontSize: Double var maxValue: Double = 100 var animateGraphic: Bool = true var animateText: Bool = true @ObservedObject private var config = Configuration.shared private var progress: Double { maxValue > 0 ? min(value / maxValue, 1.0) : 0 } private var gaugeColor: Color { if progress < 0.4 { return .green } if progress < 0.7 { return .yellow } if progress < 0.85 { return .orange } return .red } /// 高亮模式下所有文字用 gaugeColor,否则用系统颜色 private var textColor: Color { config.highlightMode ? gaugeColor : .primary } private var secondaryColor: Color { config.highlightMode ? gaugeColor : .secondary } /// 文字动画:条件应用 contentTransition @ViewBuilder private func animatedNumber(_ text: Text) -> some View { if animateText { text.contentTransition(.numericText(value: value)) } else { text } } private var graphicAnim: Animation? { animateGraphic ? .easeOut(duration: 0.2) : nil } private var textAnim: Animation? { animateText ? .easeOut(duration: 0.3) : nil } var body: some View { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 6) { Image(systemName: icon) .font(.system(size: fontSize)) .foregroundColor(secondaryColor) Text(title) .font(.system(size: fontSize, weight: .medium)) .foregroundColor(secondaryColor) Spacer() } switch style { case .bar: barView case .ring: ringView case .text: textView case .pie: pieView } } } // MARK: - 条状百分比 private var barView: some View { VStack(spacing: 2) { GeometryReader { geo in ZStack(alignment: .leading) { Capsule() .fill(Color.white.opacity(0.12)) .frame(height: 8) Capsule() .fill(LinearGradient( colors: [gaugeColor.opacity(0.8), gaugeColor], startPoint: .leading, endPoint: .trailing)) .frame(width: max(geo.size.width * CGFloat(progress), 4), height: 8) .animation(graphicAnim, value: value) } } .frame(height: 8) HStack { animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) .foregroundColor(textColor)) Spacer() Text(detail) .font(.system(size: fontSize - 2)) .foregroundColor(secondaryColor) .lineLimit(1) } .animation(textAnim, value: value) } } // MARK: - 圆环进度 private var ringView: some View { HStack(spacing: 10) { ZStack { Circle() .stroke(Color.white.opacity(0.12), lineWidth: 5) .frame(width: 48, height: 48) Circle() .trim(from: 0, to: CGFloat(progress)) .stroke(AngularGradient( colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)], center: .center), style: StrokeStyle(lineWidth: 5, lineCap: .round)) .rotationEffect(.degrees(-90)) .frame(width: 48, height: 48) .animation(graphicAnim, value: value) animatedNumber(Text(String(format: "%.0f", value)) .font(.system(size: fontSize - 1, weight: .bold, design: .monospaced)) .foregroundColor(textColor)) } .animation(textAnim, value: value) VStack(alignment: .leading, spacing: 2) { animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) .foregroundColor(textColor)) Text(detail) .font(.system(size: fontSize - 2)) .foregroundColor(secondaryColor) .lineLimit(1) } .animation(textAnim, value: value) } } // MARK: - 纯文本 private var textView: some View { HStack { animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 6, weight: .bold, design: .monospaced)) .foregroundColor(gaugeColor)) .animation(textAnim, value: value) Spacer() Text(detail) .font(.system(size: fontSize - 2)) .foregroundColor(secondaryColor) .lineLimit(1) } } // MARK: - 扇形圆盘 private var pieView: some View { HStack(spacing: 10) { ZStack { Circle() .fill(Color.white.opacity(0.05)) .frame(width: 44, height: 44) PieSlice(endAngle: .degrees(360 * progress)) .fill(gaugeColor.opacity(0.85)) .frame(width: 42, height: 42) .clipShape(Circle()) .animation(graphicAnim, value: value) animatedNumber(Text(String(format: "%.0f", value)) .font(.system(size: fontSize - 2, weight: .bold, design: .monospaced)) .foregroundColor(.white)) } .animation(textAnim, value: value) VStack(alignment: .leading, spacing: 2) { animatedNumber(Text(String(format: "%.1f%@", value, unit)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) .foregroundColor(textColor)) Text(detail) .font(.system(size: fontSize - 2)) .foregroundColor(secondaryColor) .lineLimit(1) } .animation(textAnim, value: value) } } } // MARK: - 扇形 Shape struct PieSlice: Shape { var endAngle: Angle func path(in rect: CGRect) -> Path { var path = Path() let center = CGPoint(x: rect.midX, y: rect.midY) let radius = min(rect.width, rect.height) / 2 path.move(to: center) path.addArc(center: center, radius: radius, startAngle: .degrees(-90), endAngle: endAngle - .degrees(90), clockwise: false) path.closeSubpath() return path } }