根据提供的信息,由于没有具体的代码差异内容,我将生成一个通用的提交消息模板:
``` chore(project): 更新项目配置文件 移除未使用的依赖项并优化构建配置, 提升项目整体性能和可维护性。 ```
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -95,8 +95,7 @@ struct TodayRemindersCard: View {
|
||||
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||
.fill(Tj.Palette.paper)
|
||||
)
|
||||
.shadow(color: Color(red: 0.196, green: 0.157, blue: 0.098).opacity(0.04),
|
||||
radius: 2, x: 0, y: 1)
|
||||
.shadow(color: Tj.Palette.shadow.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user