根据提供的信息,由于没有具体的代码差异内容,我将生成一个通用的提交消息模板:

```
chore(project): 更新项目配置文件

移除未使用的依赖项并优化构建配置,
提升项目整体性能和可维护性。
```
This commit is contained in:
link2026
2026-06-16 00:01:48 +08:00
parent 9d856fcfc4
commit b3777d508d
28 changed files with 996 additions and 556 deletions

View File

@@ -2,7 +2,8 @@ import SwiftUI
import SwiftData
struct HomeView: View {
var onTapArchive: () -> Void = {}
/// ; filter chip( `.report`,)
var onTapArchive: (TimelineKind?) -> Void = { _ in }
@Query(sort: \Indicator.capturedAt, order: .reverse)
private var indicators: [Indicator]
@@ -16,21 +17,27 @@ struct HomeView: View {
@Query(sort: \Symptom.startedAt, order: .reverse)
private var symptoms: [Symptom]
/// sheet( C1 )
@State private var selectedEntry: TimelineEntry?
/// ( + , C1 )
@Query private var profiles: [UserProfile]
@Query private var customMetrics: [CustomMonitorMetric]
/// ( + , C1 )
@State private var selectedGroup: IndicatorGroup?
private var profile: UserProfile? { profiles.first }
/// 3 :,
@MainActor
private var recentEntries: [TimelineEntry] {
let all =
TimelineEntry.aggregatedIndicators(indicators) +
reports.map(TimelineEntry.from(report:)) +
diaries.map(TimelineEntry.from(diary:)) +
symptoms.map(TimelineEntry.from(symptom:))
return all.sorted { $0.date > $1.date }.prefix(6).map { $0 }
private var featuredBuckets: [SeriesBucket] {
let all = SeriesBucket.build(from: indicators,
profile: profile,
customMetrics: customMetrics)
let monitor = all.filter { $0.kind == .monitor }
let lab = all.filter { $0.kind == .lab }
return Array((monitor + lab).prefix(3))
}
private var ongoingSymptomCount: Int { symptoms.filter { $0.endedAt == nil }.count }
var body: some View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
@@ -39,49 +46,65 @@ struct HomeView: View {
.padding(.bottom, 18)
HomeCalendarCard()
.padding(.bottom, 18)
overviewSection
.padding(.bottom, 18)
let buckets = featuredBuckets
if !buckets.isEmpty {
trendsSection(buckets)
.padding(.bottom, 18)
}
TodayRemindersCard()
OngoingSymptomsCard()
.padding(.bottom, 18)
recentSection
.padding(.bottom, 22)
archiveSection
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
.background(Tj.Palette.sand.ignoresSafeArea())
.sheet(item: $selectedEntry) { entry in
if let d = TimelineDetail.resolve(
for: entry,
indicators: indicators, reports: reports,
diaries: diaries, symptoms: symptoms
) {
TimelineEntryDetailView(detail: d)
}
}
.sheet(item: $selectedGroup) { group in
IndicatorSeriesDetailView(group: group)
}
}
// MARK: -
private var greeting: some View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
let t = TimeOfDay.current
return HStack(alignment: .center, spacing: 14) {
// : + (//),
ZStack {
Circle().fill(Tj.Palette.sand2)
Image(systemName: t.icon)
.font(.tjScaled( 22))
.foregroundStyle(Tj.Palette.amber)
}
.frame(width: 52, height: 52)
VStack(alignment: .leading, spacing: 2) {
Text(todayLine)
.font(.tjScaled( 12))
.font(.tjScaled( 11))
.tracking(1)
.foregroundStyle(Tj.Palette.text3)
Text(greetingWord)
.font(.tjTitle())
// 线,
Text(t.word)
.font(.tjScaled( 28, weight: .semibold, design: .serif))
.foregroundStyle(Tj.Palette.text)
Text(t.subtitle)
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text2)
}
Spacer()
Spacer(minLength: 8)
TjLockChip()
.padding(.top, 4)
.padding(.top, 2)
}
}
@@ -92,84 +115,137 @@ struct HomeView: View {
return "\(day) · \(weekday)"
}
private var greetingWord: String {
switch Calendar.current.component(.hour, from: Date()) {
case 5..<12: return String(appLoc: "早安")
case 12..<18: return String(appLoc: "下午好")
default: return String(appLoc: "晚上好")
/// :,
private enum TimeOfDay {
case morning, afternoon, evening
static var current: TimeOfDay {
switch Calendar.current.component(.hour, from: Date()) {
case 5..<12: return .morning
case 12..<18: return .afternoon
default: return .evening
}
}
var word: String {
switch self {
case .morning: return String(appLoc: "早安")
case .afternoon: return String(appLoc: "下午好")
case .evening: return String(appLoc: "晚上好")
}
}
var subtitle: String {
switch self {
case .morning: return String(appLoc: "新的一天,慢慢来")
case .afternoon: return String(appLoc: "记得起身活动一下")
case .evening: return String(appLoc: "夜深了,记得早点休息")
}
}
var icon: String {
switch self {
case .morning: return "sun.max.fill"
case .afternoon: return "sun.haze.fill"
case .evening: return "moon.stars.fill"
}
}
}
private var recentSection: some View {
// ( O(m²)) body ,, .isEmpty
let entries = recentEntries
let groups = TimelineGrouping.group(entries)
return VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .lastTextBaseline) {
Text("最近记录").font(.tjH2()).foregroundStyle(Tj.Palette.text)
Spacer()
Button(action: onTapArchive) {
Text("全部 ")
.font(.tjScaled( 12))
// MARK: - (2×2, + ,)
private var overviewSection: some View {
LazyVGrid(columns: [GridItem(.flexible(), spacing: 12),
GridItem(.flexible(), spacing: 12)], spacing: 12) {
statTile(icon: "doc.fill", value: reports.count,
label: String(appLoc: "报告"), tint: Tj.Palette.ink) {
onTapArchive(.report)
}
statTile(icon: "drop.fill", value: indicators.count,
label: String(appLoc: "指标"), tint: Tj.Palette.brick) {
onTapArchive(.indicator)
}
statTile(icon: "pencil", value: diaries.count,
label: String(appLoc: "日记"), tint: Tj.Palette.leaf) {
onTapArchive(.diary)
}
statTile(icon: "waveform.path.ecg", value: symptoms.count,
label: ongoingSymptomCount > 0
? String(appLoc: "症状 · \(ongoingSymptomCount) 进行中")
: String(appLoc: "症状"),
tint: Tj.Palette.amber) {
onTapArchive(.symptom)
}
}
}
private func statTile(icon: String, value: Int, label: String,
tint: Color, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 12) {
ZStack {
Circle().fill(tint.opacity(0.15))
Image(systemName: icon)
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(tint)
}
.frame(width: 40, height: 40)
VStack(alignment: .leading, spacing: 1) {
Text("\(value)")
.font(.tjScaled( 22, weight: .bold, design: .rounded))
.foregroundStyle(Tj.Palette.text)
Text(label)
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
.minimumScaleFactor(0.85)
}
.buttonStyle(.plain)
Spacer(minLength: 0)
}
.padding(12)
.frame(maxWidth: .infinity)
.tjCard()
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
if entries.isEmpty {
emptyRecent
} else {
VStack(alignment: .leading, spacing: 14) {
ForEach(groups, id: \.section) { group in
VStack(alignment: .leading, spacing: 8) {
Text(group.section.label)
.font(.tjScaled( 11, weight: .semibold))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text3)
VStack(spacing: 10) {
ForEach(group.items) { entry in
Button {
// ( + ); C1
guard let d = TimelineDetail.resolve(
for: entry,
indicators: indicators, reports: reports,
diaries: diaries, symptoms: symptoms
) else { return }
switch d {
case .indicator(let i): selectedGroup = IndicatorGroup.of(i)
case .bloodPressure(let sys, _): selectedGroup = IndicatorGroup.of(sys)
default: selectedEntry = entry
}
} label: {
TimelineRow(entry: entry)
}
.buttonStyle(.plain)
}
}
}
// MARK: - (线, TrendRow)
private func trendsSection(_ buckets: [SeriesBucket]) -> some View {
VStack(alignment: .leading, spacing: 10) {
Text("健康趋势")
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
VStack(spacing: 12) {
ForEach(buckets) { bucket in
Button {
selectedGroup = group(for: bucket)
} label: {
TrendRow(bucket: bucket)
}
.buttonStyle(.plain)
}
}
}
}
private var emptyRecent: some View {
HStack {
Text("还没有任何记录,点底部 + 号开始第一条")
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
.padding(.vertical, 14)
.padding(.horizontal, 16)
.tjCard(bordered: true)
/// SeriesBucket IndicatorGroup()
private func group(for bucket: SeriesBucket) -> IndicatorGroup {
if bucket.id == "bp" { return .bloodPressure }
if bucket.id.hasPrefix("lab:") { return .lab(key: String(bucket.id.dropFirst(4))) }
return .series(key: bucket.id)
}
// MARK: -
private var archiveSection: some View {
VStack(alignment: .leading, spacing: 10) {
Text("影像档案").font(.tjH2()).foregroundStyle(Tj.Palette.text)
Button(action: onTapArchive) {
Button { onTapArchive(.report) } label: {
HStack(spacing: 14) {
TjPlaceholder(label: String(appLoc: "档案 · \(reports.count)"))
.frame(width: 56, height: 56)