223 lines
7.4 KiB
Swift
223 lines
7.4 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - 通用计量器:根据 GaugeDisplayStyle 动态渲染
|
||
|
||
struct StyledGauge: View {
|
||
let title: String
|
||
let icon: String
|
||
let value: Double // 0-100
|
||
let unit: String // "%" / "GB"
|
||
let detail: String // 副标题(如 "3.2 / 16 GB")
|
||
let style: GaugeDisplayStyle
|
||
let fontSize: Double
|
||
|
||
// 主题色(根据负载渐变)
|
||
private var gaugeColor: Color {
|
||
if value < 40 { return .green }
|
||
if value < 70 { return .yellow }
|
||
if value < 85 { return .orange }
|
||
return .red
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
// 标题行
|
||
HStack(spacing: 6) {
|
||
Image(systemName: icon)
|
||
.font(.system(size: fontSize))
|
||
.foregroundColor(.secondary)
|
||
Text(title)
|
||
.font(.system(size: fontSize, weight: .medium))
|
||
.foregroundColor(.secondary)
|
||
Spacer()
|
||
}
|
||
|
||
// 根据样式渲染
|
||
switch style {
|
||
case .bar:
|
||
barView
|
||
case .ring:
|
||
ringView
|
||
case .text:
|
||
textView
|
||
case .pie:
|
||
pieView
|
||
case .compact:
|
||
compactView
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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(value / 100), 4), height: 8)
|
||
.animation(.easeOut(duration: 0.2), value: value)
|
||
}
|
||
}
|
||
.frame(height: 8)
|
||
|
||
HStack {
|
||
Text(String(format: "%.1f%@", value, unit))
|
||
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
||
.foregroundColor(.primary)
|
||
Spacer()
|
||
Text(detail)
|
||
.font(.system(size: fontSize - 2))
|
||
.foregroundColor(.secondary)
|
||
.lineLimit(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 圆环进度
|
||
|
||
private var ringView: some View {
|
||
HStack(spacing: 10) {
|
||
ZStack {
|
||
Circle()
|
||
.stroke(Color.white.opacity(0.12), lineWidth: 5)
|
||
.frame(width: 48, height: 48)
|
||
.animation(.easeOut(duration: 0.2), value: value)
|
||
Circle()
|
||
.trim(from: 0, to: CGFloat(value / 100))
|
||
.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)
|
||
|
||
Text(String(format: "%.0f", value))
|
||
.font(.system(size: fontSize - 1, weight: .bold, design: .monospaced))
|
||
.foregroundColor(.primary)
|
||
}
|
||
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(String(format: "%.1f%@", value, unit))
|
||
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
||
.foregroundColor(.primary)
|
||
Text(detail)
|
||
.font(.system(size: fontSize - 2))
|
||
.foregroundColor(.secondary)
|
||
.lineLimit(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 纯文本
|
||
|
||
private var textView: some View {
|
||
HStack {
|
||
Text(String(format: "%.1f%@", value, unit))
|
||
.font(.system(size: fontSize + 6, weight: .bold, design: .monospaced))
|
||
.foregroundColor(gaugeColor)
|
||
.animation(.easeOut(duration: 0.2), value: value)
|
||
Spacer()
|
||
Text(detail)
|
||
.font(.system(size: fontSize - 2))
|
||
.foregroundColor(.secondary)
|
||
.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 * value / 100))
|
||
.fill(gaugeColor.opacity(0.85))
|
||
.frame(width: 42, height: 42)
|
||
.clipShape(Circle())
|
||
.animation(.easeOut(duration: 0.2), value: value)
|
||
|
||
Text(String(format: "%.0f", value))
|
||
.font(.system(size: fontSize - 2, weight: .bold, design: .monospaced))
|
||
.foregroundColor(.white)
|
||
}
|
||
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(String(format: "%.1f%@", value, unit))
|
||
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
||
.foregroundColor(.primary)
|
||
Text(detail)
|
||
.font(.system(size: fontSize - 2))
|
||
.foregroundColor(.secondary)
|
||
.lineLimit(1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 紧凑横条
|
||
|
||
private var compactView: some View {
|
||
VStack(spacing: 4) {
|
||
GeometryReader { geo in
|
||
ZStack(alignment: .leading) {
|
||
RoundedRectangle(cornerRadius: 4)
|
||
.fill(Color.white.opacity(0.1))
|
||
.frame(height: 14)
|
||
RoundedRectangle(cornerRadius: 4)
|
||
.fill(gaugeColor)
|
||
.frame(width: max(geo.size.width * CGFloat(value / 100), 3), height: 14)
|
||
.animation(.easeOut(duration: 0.2), value: value)
|
||
}
|
||
}
|
||
.frame(height: 14)
|
||
|
||
HStack {
|
||
Text(String(format: "%.1f%@", value, unit))
|
||
.font(.system(size: fontSize, weight: .bold, design: .monospaced))
|
||
.foregroundColor(.primary)
|
||
Spacer()
|
||
Text(detail)
|
||
.font(.system(size: fontSize - 2))
|
||
.foregroundColor(.secondary)
|
||
.lineLimit(1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|