``` docs(readme): 更新文档说明 - 添加了项目使用指南 - 完善了API接口说明 - 修正了一些文字错误 ``` 注:由于未提供具体的代码差异信息,以上为示例格式。请提供具体的代码变更内容以便生成准确的commit message。
155 lines
5.6 KiB
Swift
155 lines
5.6 KiB
Swift
import SwiftUI
|
||
import SwiftData
|
||
|
||
/// 「我的 · 自定义指标」管理页。
|
||
/// 从 MeView 进入;集中查看 / 新建 / 编辑 / 删除自定义长期监测指标。
|
||
struct CustomMetricsListView: View {
|
||
@Query(sort: \CustomMonitorMetric.createdAt, order: .reverse)
|
||
private var metrics: [CustomMonitorMetric]
|
||
|
||
@Query private var indicators: [Indicator]
|
||
|
||
@State private var editingTarget: CustomMetricEditTarget?
|
||
|
||
var body: some View {
|
||
ScrollView {
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
hintBanner
|
||
if metrics.isEmpty {
|
||
emptyState
|
||
} else {
|
||
// 一次性按 seriesKey 统计使用次数(O(indicators)),行内 O(1) 查表,
|
||
// 取代「每行都重扫整张指标表」的 O(metrics × indicators) N+1。
|
||
let usageCounts: [String: Int] = indicators.reduce(into: [:]) { acc, ind in
|
||
if let key = ind.seriesKey, !key.isEmpty { acc[key, default: 0] += 1 }
|
||
}
|
||
ForEach(metrics) { m in
|
||
Button {
|
||
editingTarget = CustomMetricEditTarget(metric: m)
|
||
} label: {
|
||
row(m, usage: usageCounts[m.seriesKey] ?? 0)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
}
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, 8)
|
||
.padding(.bottom, 32)
|
||
}
|
||
.background(Tj.Palette.sand.ignoresSafeArea())
|
||
.navigationTitle("自定义指标")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .topBarTrailing) {
|
||
Button {
|
||
editingTarget = CustomMetricEditTarget(metric: nil)
|
||
} label: {
|
||
Image(systemName: "plus")
|
||
.font(.tjScaled( 16, weight: .semibold))
|
||
}
|
||
}
|
||
}
|
||
.sheet(item: $editingTarget) { target in
|
||
CustomMetricEditor(existing: target.metric) { _ in }
|
||
}
|
||
}
|
||
|
||
// MARK: - subviews
|
||
|
||
private var hintBanner: some View {
|
||
HStack(spacing: 10) {
|
||
Image(systemName: "info.circle.fill")
|
||
.foregroundStyle(Tj.Palette.text3)
|
||
Text("自定义指标会出现在「+ 指标记录 → 长期监测」的 grid 里,可设提醒、进趋势")
|
||
.font(.tjScaled( 12))
|
||
.foregroundStyle(Tj.Palette.text2)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
Spacer(minLength: 0)
|
||
}
|
||
.padding(12)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||
.fill(Tj.Palette.sand2.opacity(0.5))
|
||
)
|
||
}
|
||
|
||
private var emptyState: some View {
|
||
VStack(spacing: 14) {
|
||
Spacer(minLength: 40)
|
||
TjPlaceholder(label: String(appLoc: "还没有自定义指标"))
|
||
.frame(width: 220, height: 130)
|
||
Text("右上角 + 新建一个")
|
||
.font(.tjScaled( 12))
|
||
.foregroundStyle(Tj.Palette.text3)
|
||
Spacer()
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
|
||
private func row(_ m: CustomMonitorMetric, usage count: Int) -> some View {
|
||
HStack(spacing: 12) {
|
||
ZStack {
|
||
Circle().fill(Tj.Palette.leafSoft)
|
||
Image(systemName: m.icon)
|
||
.font(.tjScaled( 17, weight: .medium))
|
||
.foregroundStyle(Tj.Palette.ink)
|
||
}
|
||
.frame(width: 40, height: 40)
|
||
|
||
VStack(alignment: .leading, spacing: 3) {
|
||
Text(m.name)
|
||
.font(.tjScaled( 15, weight: .semibold))
|
||
.foregroundStyle(Tj.Palette.text)
|
||
.lineLimit(1)
|
||
HStack(spacing: 6) {
|
||
if !m.unit.isEmpty {
|
||
Text(m.unit)
|
||
.font(.tjScaled( 11, design: .monospaced))
|
||
.foregroundStyle(Tj.Palette.text3)
|
||
}
|
||
if !m.rangeText.isEmpty {
|
||
Text("·")
|
||
.font(.tjScaled( 11))
|
||
.foregroundStyle(Tj.Palette.text3)
|
||
Text(m.rangeText)
|
||
.font(.tjScaled( 11, design: .monospaced))
|
||
.foregroundStyle(Tj.Palette.text3)
|
||
}
|
||
}
|
||
}
|
||
|
||
Spacer(minLength: 8)
|
||
|
||
VStack(alignment: .trailing, spacing: 2) {
|
||
Text(count == 0 ? String(appLoc: "未使用") : String(appLoc: "用 \(count) 次"))
|
||
.font(.tjScaled( 11, weight: count > 0 ? .semibold : .regular))
|
||
.foregroundStyle(count > 0 ? Tj.Palette.ink : Tj.Palette.text3)
|
||
Image(systemName: "chevron.right")
|
||
.font(.tjScaled( 11, weight: .medium))
|
||
.foregroundStyle(Tj.Palette.text3)
|
||
}
|
||
}
|
||
.padding(14)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
|
||
.fill(Tj.Palette.paper)
|
||
)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
|
||
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
|
||
)
|
||
}
|
||
|
||
}
|
||
|
||
#Preview {
|
||
NavigationStack {
|
||
CustomMetricsListView()
|
||
}
|
||
.modelContainer(for: [
|
||
CustomMonitorMetric.self, Indicator.self,
|
||
UserProfile.self, MetricReminder.self,
|
||
], inMemory: true)
|
||
}
|