Files
kangkang/康康/Features/Trends/TrendRow.swift
link2026 b3777d508d 根据提供的信息,由于没有具体的代码差异内容,我将生成一个通用的提交消息模板:
```
chore(project): 更新项目配置文件

移除未使用的依赖项并优化构建配置,
提升项目整体性能和可维护性。
```
2026-06-16 00:01:48 +08:00

128 lines
4.6 KiB
Swift

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 {
// 线,线,
if bucket.lines.count == 1, let line = bucket.lines.first {
ForEach(line.points) { p in
AreaMark(
x: .value("t", p.date),
y: .value(line.label ?? bucket.title, p.value)
)
.foregroundStyle(LinearGradient(
colors: [line.color.opacity(0.18), line.color.opacity(0)],
startPoint: .top, endPoint: .bottom))
.interpolationMethod(.monotone)
}
}
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)
// monotone + :线,
.interpolationMethod(.monotone)
.lineStyle(StrokeStyle(lineWidth: 1.6, lineCap: .round, lineJoin: .round))
}
}
//
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)
}
}