import SwiftUI import Charts /// 趋势列表的紧凑行:名称 + 条数/跨度 + mini sparkline + 最新值。 struct TrendRow: View { let bucket: SeriesBucket private var allPoints: [SeriesBucket.Point] { bucket.lines.flatMap(\.points) } private var pointCount: Int { allPoints.count } private var anyLatestAbnormal: Bool { bucket.lines.contains { ($0.latestPoint?.status ?? .normal) != .normal } } var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 3) { Text(bucket.title) .font(.tjScaled( 15, weight: .semibold)) .foregroundStyle(Tj.Palette.text) .lineLimit(1) Text(subtitle) .font(.tjScaled( 11)) .foregroundStyle(Tj.Palette.text3) } Spacer(minLength: 8) sparkline .frame(width: 76, height: 34) VStack(alignment: .trailing, spacing: 2) { Text(latestValue) .font(.tjScaled( 14, weight: .semibold, design: .monospaced)) .foregroundStyle(anyLatestAbnormal ? Tj.Palette.brick : Tj.Palette.text) .lineLimit(1) Text(bucket.unit) .font(.tjScaled( 9, design: .monospaced)) .foregroundStyle(Tj.Palette.text3) } .fixedSize() Image(systemName: "chevron.right") .font(.tjScaled( 12, weight: .medium)) .foregroundStyle(Tj.Palette.text3) } .padding(14) .frame(maxWidth: .infinity) .tjCard(bordered: true) } private var sparkline: some View { Chart { ForEach(bucket.lines) { line in ForEach(line.points) { p in LineMark( x: .value("t", p.date), y: .value(line.label ?? bucket.title, p.value), series: .value("s", line.id) ) .foregroundStyle(line.color) .interpolationMethod(.catmullRom) .lineStyle(StrokeStyle(lineWidth: 1.6)) } } // 最新点高亮 ForEach(bucket.lines) { line in if let p = line.latestPoint { PointMark( x: .value("t", p.date), y: .value("v", p.value) ) .foregroundStyle(p.status == .normal ? line.color : Tj.Palette.brick) .symbolSize(28) } } } .chartXAxis(.hidden) .chartYAxis(.hidden) .chartLegend(.hidden) } private var subtitle: String { "\(pointCount) 条 · 近 \(spanLabel)" } private var spanLabel: String { let dates = allPoints.map(\.date) guard let lo = dates.min(), let hi = dates.max() else { return "—" } let days = Calendar.current.dateComponents([.day], from: lo, to: hi).day ?? 0 if days <= 0 { return String(appLoc: "今天") } if days < 30 { return String(appLoc: "\(days) 天") } if days < 365 { return String(appLoc: "\(days / 30) 个月") } return String(appLoc: "\(days / 365) 年") } private var latestValue: String { let parts = bucket.lines.compactMap { line -> String? in guard let p = line.latestPoint else { return nil } return formatValue(p.value) } return parts.joined(separator: "/") } private func formatValue(_ v: Double) -> String { v.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", v) : String(format: "%.1f", v) } }