缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。

当您提供代码差异后,我将按照以下格式生成:

```
<type>(<scope>): <subject>

<body>
```

其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
This commit is contained in:
link2026
2026-06-07 14:17:18 +08:00
parent 074d99715d
commit 77a4ee1c37
66 changed files with 2676 additions and 548 deletions

View File

@@ -30,7 +30,6 @@ struct ArchiveListView: View {
@State private var filter: TimelineKind? = nil
@State private var endingSymptom: Symptom?
@State private var selectedEntry: TimelineEntry?
@State private var showExportSheet = false
@State private var route: Route?
@MainActor
@@ -110,9 +109,6 @@ struct ArchiveListView: View {
TimelineEntryDetailView(detail: d)
}
}
.fullScreenCover(isPresented: $showExportSheet) {
HealthExportSheet()
}
}
@ViewBuilder
@@ -150,35 +146,23 @@ struct ArchiveListView: View {
.font(.tjTitle(26))
.foregroundStyle(Tj.Palette.text)
Text(totalCount == 0 ? "" : String(appLoc: "\(totalCount)"))
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
Menu {
Button {
showExportSheet = true
} label: {
Label("生成新导出", systemImage: "doc.text.below.ecg")
}
if !exports.isEmpty {
Button {
route = .exports
} label: {
Label("我的导出 · \(exports.count)", systemImage: "clock.arrow.circlepath")
if !exports.isEmpty {
Button { route = .exports } label: {
HStack(spacing: 6) {
Image(systemName: "clock.arrow.circlepath")
.font(.tjScaled( 12, weight: .semibold))
Text("导出历史")
.font(.tjScaled( 13, weight: .semibold))
}
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(Capsule().fill(Tj.Palette.ink))
}
} label: {
HStack(spacing: 6) {
Image(systemName: "doc.text.below.ecg")
.font(.system(size: 12, weight: .semibold))
Text("导出身体档案")
.font(.system(size: 13, weight: .semibold))
Image(systemName: "chevron.down")
.font(.system(size: 9, weight: .semibold))
}
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(Capsule().fill(Tj.Palette.ink))
.buttonStyle(.plain)
}
}
}
@@ -217,19 +201,19 @@ struct ArchiveListView: View {
ZStack {
Circle().fill(reminderEnabledCount > 0 ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "bell.fill")
.font(.system(size: 16))
.font(.tjScaled( 16))
.foregroundStyle(reminderEnabledCount > 0 ? Tj.Palette.ink : Tj.Palette.text3)
}
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
Text(reminderCountLabel)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
if !reminderTitlePreview.isEmpty {
Text(reminderTitleLine)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
}
@@ -238,7 +222,7 @@ struct ArchiveListView: View {
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
@@ -265,7 +249,7 @@ struct ArchiveListView: View {
private func chip(label: String, selected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(label)
.font(.system(size: 13, weight: selected ? .semibold : .regular))
.font(.tjScaled( 13, weight: selected ? .semibold : .regular))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text)
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -282,14 +266,14 @@ struct ArchiveListView: View {
private func sectionHeader(_ section: DateSection, count: Int) -> some View {
HStack {
Text(section.label)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text2)
Rectangle()
.fill(Tj.Palette.lineSoft)
.frame(height: 1)
Text("\(count)")
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.horizontal, 20)
@@ -303,7 +287,7 @@ struct ArchiveListView: View {
TjPlaceholder(label: String(appLoc: "还没有任何记录\n点底部 + 号开始"))
.frame(width: 240, height: 140)
Text(filter == nil ? String(appLoc: "记录会按时间归类显示") : String(appLoc: "这个类别下没有记录"))
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}

View File

@@ -52,7 +52,7 @@ struct HealthExportDetailView: View {
HStack(alignment: .center, spacing: 12) {
Button { dismiss() } label: {
Image(systemName: "xmark")
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.frame(width: 32, height: 32)
.background(Circle().fill(Tj.Palette.sand2))
@@ -62,7 +62,7 @@ struct HealthExportDetailView: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text(Self.absoluteDate(export.createdAt))
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
@@ -81,13 +81,13 @@ struct HealthExportDetailView: View {
TjBadge(text: export.modelTag, style: .neutral)
if export.decodeRate > 0 {
Text(String(format: "%.1f tok/s", export.decodeRate))
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.leaf)
}
Spacer()
if let from = export.inferredTimeFromDate, let to = export.inferredTimeToDate {
Text("\(Self.shortDate(from))\(Self.shortDate(to))")
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -96,10 +96,10 @@ struct HealthExportDetailView: View {
private var promptBlock: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "quote.opening")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Text(export.prompt)
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
}
.padding(12)
@@ -119,7 +119,7 @@ struct HealthExportDetailView: View {
ShareLink(item: export.content) {
Label("分享", systemImage: "square.and.arrow.up")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.tracking(1)
.foregroundStyle(Tj.Palette.ink)
.padding(.horizontal, 14)
@@ -134,7 +134,7 @@ struct HealthExportDetailView: View {
showDeleteConfirm = true
} label: {
Image(systemName: "trash")
.font(.system(size: 15, weight: .medium))
.font(.tjScaled( 15, weight: .medium))
.foregroundStyle(Tj.Palette.brick)
.frame(width: 44, height: 44)
.background(Circle().strokeBorder(Tj.Palette.brick.opacity(0.4), lineWidth: 1))

View File

@@ -57,7 +57,7 @@ struct HealthExportListView: View {
.font(.tjTitle(24))
.foregroundStyle(Tj.Palette.text)
Text(exports.isEmpty ? "" : String(appLoc: "\(exports.count)"))
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
TjLockChip()
@@ -88,22 +88,22 @@ struct HealthExportRow: View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .top) {
Text(export.promptPreview)
.font(.system(size: 14, weight: .semibold))
.font(.tjScaled( 14, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(2)
.multilineTextAlignment(.leading)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.font(.tjScaled( 12, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
HStack(spacing: 8) {
Text(Self.relativeDate(export.createdAt))
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
if export.decodeRate > 0 {
Text(String(format: "%.1f tok/s", export.decodeRate))
.font(.system(size: 10, design: .monospaced))
.font(.tjScaled( 10, design: .monospaced))
.foregroundStyle(Tj.Palette.leaf)
}
Spacer()

View File

@@ -2,7 +2,7 @@ import SwiftUI
import SwiftData
/// sheet
/// :idle running(extractingIntent retrieving generating) completed / failed
/// : running(retrieving generating) completed / failed
struct HealthExportSheet: View {
@Environment(\.modelContext) private var ctx
@Environment(\.dismiss) private var dismiss
@@ -10,7 +10,8 @@ struct HealthExportSheet: View {
/// :(,W3 )
let initialPrompt: String
@State private var prompt: String = ""
@State private var turns: [HealthExportDialogueTurn] = []
@State private var draftQuestion: String = ""
@State private var phase: HealthExportService.Phase?
@State private var content: String = ""
@State private var rate: Double = 0
@@ -18,14 +19,25 @@ struct HealthExportSheet: View {
@State private var error: Error?
@State private var completed: Bool = false
@State private var copiedFlash: Bool = false
@FocusState private var promptFocused: Bool
@State private var answeringTurnID: UUID?
@FocusState private var questionFocused: Bool
init(initialPrompt: String = "") {
self.initialPrompt = initialPrompt
}
private var isRunning: Bool { phase != nil && !completed && error == nil }
private var isInputMode: Bool { phase == nil && !completed && error == nil }
private var isGeneratingReport: Bool { phase != nil && !completed && error == nil }
private var isAnswering: Bool { answeringTurnID != nil }
private var canAsk: Bool {
!isAnswering &&
!isGeneratingReport &&
!draftQuestion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
private var canGenerateReport: Bool {
!isAnswering &&
!isGeneratingReport &&
turns.contains(where: { $0.role == .user && !$0.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })
}
var body: some View {
VStack(spacing: 0) {
@@ -33,28 +45,20 @@ struct HealthExportSheet: View {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading, spacing: 18) {
if isInputMode {
inputSection
} else {
promptEcho
if isRunning { phaseIndicator }
if !content.isEmpty {
MarkdownView(text: content)
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.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)
)
}
if let err = error { errorRow(err) }
// ,
Color.clear.frame(height: 1).id("bottom")
introSection
ForEach(turns) { turn in
dialogueBubble(turn)
}
if isGeneratingReport { phaseIndicator }
if !content.isEmpty {
reportCard
}
if let err = error { errorRow(err) }
Color.clear.frame(height: 1).id("bottom")
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
@@ -64,13 +68,24 @@ struct HealthExportSheet: View {
proxy.scrollTo("bottom", anchor: .bottom)
}
}
.onChange(of: turns) { _, _ in
withAnimation(.easeOut(duration: 0.12)) {
proxy.scrollTo("bottom", anchor: .bottom)
}
}
}
if completed {
actionRow
} else {
composer
}
if completed { actionRow }
}
.background(Tj.Palette.sand.ignoresSafeArea())
.onAppear {
if prompt.isEmpty { prompt = initialPrompt }
if isInputMode { promptFocused = true }
if !initialPrompt.isEmpty, draftQuestion.isEmpty, turns.isEmpty {
draftQuestion = initialPrompt
}
questionFocused = true
}
.onDisappear { task?.cancel() }
}
@@ -81,17 +96,17 @@ struct HealthExportSheet: View {
HStack(alignment: .center, spacing: 12) {
Button { close() } label: {
Image(systemName: "xmark")
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.frame(width: 32, height: 32)
.background(Circle().fill(Tj.Palette.sand2))
}
VStack(alignment: .leading, spacing: 2) {
Text("导出身体档案")
Text("身体档案")
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("给医生看的就诊摘要")
.font(.system(size: 11))
Text("先问清楚,再整理给医生")
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
@@ -105,82 +120,92 @@ struct HealthExportSheet: View {
}
}
// MARK: - Input section (idle)
// MARK: - Dialogue
private var inputSection: some View {
private var introSection: some View {
VStack(alignment: .leading, spacing: 14) {
Text("说说你想给医生看什么")
.font(.system(size: 13, weight: .semibold))
Text("围绕你的指标和健康日记提问")
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
VStack(alignment: .leading, spacing: 6) {
Text("例:我感冒3天了,把最近一个月的健康情况给医生看")
.font(.system(size: 12))
Text("例:最近血压波动大吗?")
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Text("例:最近血糖好像不稳,把过去三个月的化验单整理一下")
.font(.system(size: 12))
Text("例:把我最近头晕、睡眠和指标变化整理给医生")
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
ZStack(alignment: .topLeading) {
if prompt.isEmpty {
Text("在这里输入主诉……")
.font(.system(size: 15))
.foregroundStyle(Tj.Palette.text3)
.padding(.horizontal, 14)
.padding(.vertical, 14)
.allowsHitTesting(false)
Text("上下文:全部记录指标 + 健康日记 · 本地 RAG · 不上传任何数据")
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.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)
)
}
private func dialogueBubble(_ turn: HealthExportDialogueTurn) -> some View {
let isUser = turn.role == .user
return HStack(alignment: .top, spacing: 8) {
if isUser { Spacer(minLength: 44) }
VStack(alignment: .leading, spacing: 6) {
Text(turn.role.transcriptLabel)
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(isUser ? Tj.Palette.paper.opacity(0.8) : Tj.Palette.text3)
if turn.id == answeringTurnID && turn.text.isEmpty {
HStack(spacing: 8) {
ProgressView()
Text("正在查看本地记录…")
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
}
} else {
Text(turn.text)
.font(.tjScaled( 14))
.lineSpacing(3)
.foregroundStyle(isUser ? Tj.Palette.paper : Tj.Palette.text)
.fixedSize(horizontal: false, vertical: true)
}
TextEditor(text: $prompt)
.font(.system(size: 15))
.foregroundStyle(Tj.Palette.text)
.scrollContentBackground(.hidden)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.frame(minHeight: 130)
.focused($promptFocused)
}
.padding(12)
.frame(maxWidth: 300, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.paper)
.fill(isUser ? Tj.Palette.ink : Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
.strokeBorder(isUser ? Color.clear : Tj.Palette.lineSoft, lineWidth: 1)
)
HStack {
Text("本地 RAG · Qwen3 1.7B · 不上传任何数据")
.font(.system(size: 11))
.foregroundStyle(Tj.Palette.text3)
Spacer()
Button { start() } label: {
Text("生成报告")
}
.buttonStyle(TjPrimaryButton(height: 44, fontSize: 14))
.disabled(prompt.trimmingCharacters(in: .whitespaces).isEmpty)
.opacity(prompt.trimmingCharacters(in: .whitespaces).isEmpty ? 0.5 : 1)
}
if !isUser { Spacer(minLength: 44) }
}
}
// MARK: - Prompt echo (after start)
private var promptEcho: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "quote.opening")
.font(.system(size: 12))
.foregroundStyle(Tj.Palette.text3)
Text(prompt)
.font(.system(size: 13))
private var reportCard: some View {
VStack(alignment: .leading, spacing: 10) {
Text("整理好的报告")
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
.lineLimit(3)
MarkdownView(text: content)
}
.padding(12)
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(Tj.Palette.sand2)
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)
)
}
@@ -197,11 +222,11 @@ struct HealthExportSheet: View {
}
if phase == .generating && rate > 0 {
Text(String(format: String(appLoc: "本地推理 · %.1f tok/s"), rate))
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.leaf)
} else {
Text(phase?.label ?? "")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -213,7 +238,7 @@ struct HealthExportSheet: View {
let fill = active ? Tj.Palette.ink : (done ? Tj.Palette.leaf : Tj.Palette.sand2)
let fg = (active || done) ? Tj.Palette.paper : Tj.Palette.text3
return Text(p.label)
.font(.system(size: 11, weight: active ? .semibold : .regular))
.font(.tjScaled( 11, weight: active ? .semibold : .regular))
.foregroundStyle(fg)
.padding(.horizontal, 10)
.padding(.vertical, 5)
@@ -222,7 +247,7 @@ struct HealthExportSheet: View {
private var arrow: some View {
Image(systemName: "chevron.right")
.font(.system(size: 10, weight: .semibold))
.font(.tjScaled( 10, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
@@ -243,7 +268,7 @@ struct HealthExportSheet: View {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(Tj.Palette.brick)
Text(err.localizedDescription)
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text)
}
Button { reset() } label: { Text("返回修改") }
@@ -268,7 +293,7 @@ struct HealthExportSheet: View {
ShareLink(item: content) {
Label("分享", systemImage: "square.and.arrow.up")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.tracking(1)
.foregroundStyle(Tj.Palette.ink)
.padding(.horizontal, 14)
@@ -279,7 +304,7 @@ struct HealthExportSheet: View {
Spacer()
Button { regenerate() } label: {
Label("重新生成", systemImage: "arrow.clockwise")
Label("重新整理", systemImage: "arrow.clockwise")
}
.buttonStyle(TjPrimaryButton(height: 44, fontSize: 13, horizontalPadding: 16))
}
@@ -291,19 +316,100 @@ struct HealthExportSheet: View {
}
}
private var composer: some View {
VStack(spacing: 10) {
HStack(spacing: 8) {
TextField("继续提问或补充情况…", text: $draftQuestion, axis: .vertical)
.font(.tjScaled( 14))
.lineLimit(1...4)
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.sand2)
)
.focused($questionFocused)
.disabled(isAnswering || isGeneratingReport)
Button { sendQuestion() } label: {
Image(systemName: "arrow.up")
.font(.tjScaled( 15, weight: .bold))
.foregroundStyle(Tj.Palette.paper)
.frame(width: 40, height: 40)
.background(Circle().fill(canAsk ? Tj.Palette.ink : Tj.Palette.line))
}
.disabled(!canAsk)
.accessibilityLabel("发送问题")
}
Button { startReportGeneration() } label: {
Label("生成整理报告", systemImage: "doc.text.below.ecg")
}
.buttonStyle(TjPrimaryButton(height: 44, fontSize: 14))
.disabled(!canGenerateReport)
.opacity(canGenerateReport ? 1 : 0.45)
}
.padding(.horizontal, 20)
.padding(.vertical, 12)
.background(Tj.Palette.paper)
.overlay(alignment: .top) {
Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1)
}
}
// MARK: - Actions
private func start() {
let p = prompt.trimmingCharacters(in: .whitespacesAndNewlines)
guard !p.isEmpty else { return }
promptFocused = false
private func sendQuestion() {
let question = draftQuestion.trimmingCharacters(in: .whitespacesAndNewlines)
guard !question.isEmpty, !isAnswering, !isGeneratingReport else { return }
draftQuestion = ""
questionFocused = false
let userTurn = HealthExportDialogueTurn.user(question)
let assistantTurn = HealthExportDialogueTurn.assistant("")
turns.append(userTurn)
turns.append(assistantTurn)
answeringTurnID = assistantTurn.id
let conversationForPrompt = turns.filter { $0.id != assistantTurn.id }
let stream = HealthExportService.shared.answer(
question: question,
conversation: conversationForPrompt,
in: ctx
)
task?.cancel()
task = Task { @MainActor in
do {
for try await chunk in stream {
appendToTurn(id: assistantTurn.id, text: chunk.text)
if chunk.decodeRate > 0 { rate = chunk.decodeRate }
}
answeringTurnID = nil
questionFocused = true
} catch {
answeringTurnID = nil
appendToTurn(id: assistantTurn.id, text: error.localizedDescription)
questionFocused = true
}
}
}
private func appendToTurn(id: UUID, text: String) {
guard let idx = turns.firstIndex(where: { $0.id == id }) else { return }
turns[idx].text += text
}
private func startReportGeneration() {
guard canGenerateReport else { return }
questionFocused = false
content = ""
rate = 0 // , tok/s
error = nil
completed = false
phase = .extractingIntent
phase = .retrieving
let stream = HealthExportService.shared.export(prompt: p, in: ctx)
let stream = HealthExportService.shared.export(conversation: turns, in: ctx)
task?.cancel()
task = Task { @MainActor in
do {
for try await event in stream {
@@ -326,7 +432,7 @@ struct HealthExportSheet: View {
private func regenerate() {
completed = false
start()
startReportGeneration()
}
private func reset() {
@@ -337,7 +443,8 @@ struct HealthExportSheet: View {
rate = 0
error = nil
completed = false
promptFocused = true
answeringTurnID = nil
questionFocused = true
}
private func copy() {
@@ -377,7 +484,7 @@ struct MarkdownView: View {
case .h1(let s):
VStack(alignment: .leading, spacing: 8) {
Text(inline(s))
.font(.system(size: 22, weight: .bold))
.font(.tjScaled( 22, weight: .bold))
.foregroundStyle(Tj.Palette.text)
.fixedSize(horizontal: false, vertical: true)
Rectangle()
@@ -394,7 +501,7 @@ struct MarkdownView: View {
.fill(Tj.Palette.brick)
.frame(width: 3, height: 16)
Text(inline(s))
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
}
.padding(.top, 10)
@@ -404,10 +511,10 @@ struct MarkdownView: View {
if let abnormalText = Self.extractAbnormal(s) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.brick)
Text(inline(abnormalText))
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 0)
@@ -431,7 +538,7 @@ struct MarkdownView: View {
.frame(width: 4, height: 4)
.padding(.top, 6)
Text(inline(s))
.font(.system(size: 14))
.font(.tjScaled( 14))
.foregroundStyle(Tj.Palette.text)
.fixedSize(horizontal: false, vertical: true)
}
@@ -440,7 +547,7 @@ struct MarkdownView: View {
case .body(let s):
Text(inline(s))
.font(.system(size: 14))
.font(.tjScaled( 14))
.lineSpacing(3)
.foregroundStyle(Tj.Palette.text)
.fixedSize(horizontal: false, vertical: true)

View File

@@ -82,7 +82,7 @@ struct CalendarOverviewView: View {
}
} label: {
Text("回到今天")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -90,7 +90,7 @@ struct CalendarOverviewView: View {
if let onClose {
Button(action: onClose) {
Text("完成")
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
}
}
@@ -136,7 +136,7 @@ struct CalendarOverviewView: View {
}
} label: {
Text(m.label)
.font(.system(size: 13, weight: mode == m ? .semibold : .regular))
.font(.tjScaled( 13, weight: mode == m ? .semibold : .regular))
.foregroundStyle(mode == m ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity)
.padding(.vertical, 9)
@@ -157,7 +157,7 @@ struct CalendarOverviewView: View {
HStack {
Button { shiftAnchor(-1) } label: {
Image(systemName: "chevron.left")
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.frame(width: 36, height: 36)
.background(Circle().fill(Tj.Palette.paper))
@@ -177,7 +177,7 @@ struct CalendarOverviewView: View {
Button { shiftAnchor(1) } label: {
Image(systemName: "chevron.right")
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.frame(width: 36, height: 36)
.background(Circle().fill(Tj.Palette.paper))
@@ -230,7 +230,7 @@ struct CalendarOverviewView: View {
private var legend: some View {
VStack(alignment: .leading, spacing: 8) {
Text("图例")
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text3)
HStack(spacing: 14) {
@@ -249,7 +249,7 @@ struct CalendarOverviewView: View {
.fill(color)
.frame(width: 14, height: 6)
Text(label)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text2)
}
}

View File

@@ -40,7 +40,7 @@ struct CaptureReviewForm: View {
.foregroundStyle(Tj.Palette.amber)
VStack(alignment: .leading, spacing: 8) {
Text(text)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text2)
.fixedSize(horizontal: false, vertical: true)
if let onReanalyze {
@@ -48,7 +48,7 @@ struct CaptureReviewForm: View {
onReanalyze()
} label: {
Label("重新识别", systemImage: "arrow.clockwise")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
}
.buttonStyle(.plain)
.foregroundStyle(Tj.Palette.ink)
@@ -131,7 +131,7 @@ struct CaptureReviewForm: View {
private func labeledField<C: View>(_ label: String, @ViewBuilder content: () -> C) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(label)
.font(.system(size: 11, weight: .medium))
.font(.tjScaled( 11, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
content()
}
@@ -150,14 +150,14 @@ struct CaptureReviewForm: View {
)
} label: {
Label("加一项", systemImage: "plus.circle")
.font(.system(size: 12, weight: .medium))
.font(.tjScaled( 12, weight: .medium))
}
.buttonStyle(.plain)
.foregroundStyle(Tj.Palette.ink)
}
if parsed.indicators.isEmpty {
Text("没有指标 — 点上方「加一项」补一行,或直接保存只存图片")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.padding(.vertical, 8)
} else {
@@ -175,7 +175,7 @@ struct CaptureReviewForm: View {
return VStack(spacing: 8) {
HStack(spacing: 8) {
TextField("指标名", text: binding.name)
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
Button(role: .destructive) {
parsed.indicators.removeAll { $0.id == id }
} label: {
@@ -187,7 +187,7 @@ struct CaptureReviewForm: View {
HStack(spacing: 8) {
TextField("数值", text: binding.value)
.keyboardType(.decimalPad)
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.font(.tjScaled( 14, weight: .semibold, design: .monospaced))
.frame(maxWidth: 90)
TextField("单位", text: binding.unit)
.frame(maxWidth: 80)
@@ -247,7 +247,7 @@ struct CaptureReviewForm: View {
private func sectionLabel(_ t: String) -> some View {
Text(t)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
}

View File

@@ -13,10 +13,10 @@ struct PhotoPickerSheet: View {
var body: some View {
VStack(spacing: 20) {
Image(systemName: "photo.on.rectangle.angled")
.font(.system(size: 56))
.font(.tjScaled( 56))
.foregroundStyle(Tj.Palette.text3)
Text("模拟器没有摄像头,从相册选一张化验单/体检报告")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
.multilineTextAlignment(.center)
@@ -24,7 +24,7 @@ struct PhotoPickerSheet: View {
maxSelectionCount: 5,
matching: .images) {
Text("从相册选 ≤5 张")
.font(.system(size: 14, weight: .semibold))
.font(.tjScaled( 14, weight: .semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Tj.Palette.ink)

View File

@@ -300,7 +300,12 @@ struct UnifiedCaptureFlow: View {
status: ind.status,
capturedAt: final.reportDate,
report: report,
source: .report
source: .report,
sourcePageIndex: ind.sourcePageIndex,
sourceBoxX: ind.sourceBoxX,
sourceBoxY: ind.sourceBoxY,
sourceBoxWidth: ind.sourceBoxWidth,
sourceBoxHeight: ind.sourceBoxHeight
)
ctx.insert(i)
}
@@ -346,16 +351,16 @@ private struct AnalyzingView: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("\(images.count) 页 · 100% 本地推理 · 已用 \(elapsed)s")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
if elapsed >= timeoutSeconds - 5 {
Text("快超时了,>\(timeoutSeconds)s 会自动转为手动录入")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.amber)
}
}
Button("取消识别 · 改为手动录入", action: onCancel)
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
.padding(.top, 4)
Spacer()
@@ -375,7 +380,7 @@ private struct CaptureTipSheet: View {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 10) {
Image(systemName: "doc.viewfinder")
.font(.system(size: 28))
.font(.tjScaled( 28))
.foregroundStyle(Tj.Palette.ink)
Text("拍报告的小贴士")
.font(.tjH2())

View File

@@ -62,12 +62,12 @@ struct DiaryQuickSheet: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("记录身体状态 · 可让 AI 多轮辅助查漏补缺")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
Text("本机保存")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.horizontal, 20)
@@ -154,18 +154,18 @@ struct DiaryQuickSheet: View {
// section header
HStack(spacing: 6) {
Image(systemName: "sparkles")
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
sectionLabel(String(appLoc: "AI 辅助 · 医生角度查漏补缺"))
Spacer()
if hasQuestions {
Text("\(questions.count) 个建议")
.font(.system(size: 10, design: .monospaced))
.font(.tjScaled( 10, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
if lastRate > 0 {
Text(String(format: "%.1f tok/s", lastRate))
.font(.system(size: 10, design: .monospaced))
.font(.tjScaled( 10, design: .monospaced))
.foregroundStyle(Tj.Palette.leaf)
}
}
@@ -187,10 +187,10 @@ struct DiaryQuickSheet: View {
if exhaustedNote {
HStack(spacing: 6) {
Image(systemName: "checkmark.seal.fill")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.leaf)
Text("已覆盖主要问诊维度;补充原文后可再追问")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
Spacer(minLength: 0)
}
@@ -219,11 +219,11 @@ struct DiaryQuickSheet: View {
HStack(spacing: 10) {
ProgressView().controlSize(.small)
Text("AI 思考中… 本地推理,通常 5-10 秒")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
Spacer()
Button("取消") { cancelSuggestions() }
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.vertical, 11)
@@ -253,13 +253,13 @@ struct DiaryQuickSheet: View {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(Tj.Palette.brick)
Text(err.localizedDescription)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text)
Spacer()
}
Button { requestSuggestions() } label: {
Text("重试")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.ink)
}
.buttonStyle(.plain)
@@ -282,7 +282,7 @@ struct DiaryQuickSheet: View {
Image(systemName: icon)
Text(label)
}
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(enabled ? Tj.Palette.ink : Tj.Palette.text3)
.frame(maxWidth: .infinity)
.padding(.vertical, 11)
@@ -315,12 +315,12 @@ struct DiaryQuickSheet: View {
HStack(spacing: 8) {
HStack(spacing: 6) {
Image(systemName: round == 1 ? "1.circle.fill" : "arrow.triangle.2.circlepath")
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
Text(round == 1
? String(appLoc: "第 1 轮 · \(count)")
: String(appLoc: "\(round) 轮 · 基于你刚才更新的文本 · \(count)"))
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
}
@@ -344,10 +344,10 @@ struct DiaryQuickSheet: View {
return VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .top, spacing: 8) {
Text("\(index).")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.tjScaled( 13, weight: .semibold, design: .monospaced))
.foregroundStyle(adopted ? Tj.Palette.text3 : Tj.Palette.brick)
Text(question.q)
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(adopted ? Tj.Palette.text3 : Tj.Palette.text)
.strikethrough(adopted, color: Tj.Palette.text3)
.fixedSize(horizontal: false, vertical: true)
@@ -356,9 +356,9 @@ struct DiaryQuickSheet: View {
if adopted {
HStack(spacing: 4) {
Image(systemName: "checkmark")
.font(.system(size: 10, weight: .bold))
.font(.tjScaled( 10, weight: .bold))
Text("已采纳")
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
}
.foregroundStyle(Tj.Palette.leaf)
.padding(.horizontal, 8)
@@ -368,9 +368,9 @@ struct DiaryQuickSheet: View {
Button { adopt(question) } label: {
HStack(spacing: 4) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 12))
.font(.tjScaled( 12))
Text("采纳")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
}
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, 10)
@@ -390,10 +390,10 @@ struct DiaryQuickSheet: View {
} else if !question.fill.isEmpty && !adopted {
HStack(alignment: .top, spacing: 4) {
Text("将追加:")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
Text(question.fill)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text2)
.fixedSize(horizontal: false, vertical: true)
}
@@ -416,7 +416,7 @@ struct DiaryQuickSheet: View {
private func sectionLabel(_ text: String) -> some View {
Text(text)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
}

View File

@@ -99,7 +99,7 @@ struct QuestionFillPanel: View {
VStack(alignment: .leading, spacing: 10) {
// :,线
previewText
.font(.system(size: 13))
.font(.tjScaled( 13))
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(10)
@@ -115,7 +115,7 @@ struct QuestionFillPanel: View {
HStack(spacing: 8) {
Button(action: onCancel) {
Text("取消")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
.frame(maxWidth: .infinity)
.padding(.vertical, 9)
@@ -134,9 +134,9 @@ struct QuestionFillPanel: View {
} label: {
HStack(spacing: 5) {
Image(systemName: "text.append")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
Text("加入记录")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
}
.foregroundStyle(Tj.Palette.paper)
.frame(maxWidth: .infinity)
@@ -180,7 +180,7 @@ struct QuestionFillPanel: View {
private func slotEditor(index: Int, label: String, options: [String]) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(label)
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
if !options.isEmpty {
@@ -189,7 +189,7 @@ struct QuestionFillPanel: View {
let picked = bindingValue(index) == opt
Button { values[index] = opt } label: {
Text(opt)
.font(.system(size: 12, weight: picked ? .semibold : .regular))
.font(.tjScaled( 12, weight: picked ? .semibold : .regular))
.foregroundStyle(picked ? Tj.Palette.paper : Tj.Palette.text)
.padding(.horizontal, 10)
.padding(.vertical, 5)
@@ -208,7 +208,7 @@ struct QuestionFillPanel: View {
}
TextField(String(appLoc: "填写\(label)"), text: binding(index))
.font(.system(size: 13))
.font(.tjScaled( 13))
.padding(.horizontal, 12)
.padding(.vertical, 9)
.background(

View File

@@ -85,10 +85,10 @@ struct HomeCalendarCard: View {
Spacer()
HStack(spacing: 3) {
Text(summaryLine)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Image(systemName: "chevron.right")
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -118,7 +118,7 @@ struct HomeCalendarCard: View {
} label: {
VStack(spacing: 5) {
Text(weekdayLabel(day))
.font(.system(size: 10, weight: .medium))
.font(.tjScaled( 10, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
ZStack {
RoundedRectangle(cornerRadius: 9, style: .continuous)
@@ -128,7 +128,7 @@ struct HomeCalendarCard: View {
.strokeBorder(Tj.Palette.ink, lineWidth: 1.2)
}
Text("\(calendar.component(.day, from: day))")
.font(.system(size: 14, weight: isToday ? .bold : .regular))
.font(.tjScaled( 14, weight: isToday ? .bold : .regular))
.foregroundStyle(isToday ? Tj.Palette.ink : Tj.Palette.text)
}
.frame(height: 38)

View File

@@ -71,7 +71,7 @@ struct HomeView: View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
Text(todayLine)
.font(.system(size: 12))
.font(.tjScaled( 12))
.tracking(1)
.foregroundStyle(Tj.Palette.text3)
Text(greetingWord)
@@ -106,7 +106,7 @@ struct HomeView: View {
Spacer()
Button(action: onTapArchive) {
Text("全部 ")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
.buttonStyle(.plain)
@@ -119,7 +119,7 @@ struct HomeView: View {
ForEach(recentGrouped, id: \.section) { group in
VStack(alignment: .leading, spacing: 8) {
Text(group.section.label)
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text3)
VStack(spacing: 10) {
@@ -148,7 +148,7 @@ struct HomeView: View {
private var emptyRecent: some View {
HStack {
Text("还没有任何记录,点底部 + 号开始第一条")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
@@ -167,15 +167,15 @@ struct HomeView: View {
.frame(width: 56, height: 56)
VStack(alignment: .leading, spacing: 2) {
Text("我的报告档案")
.font(.system(size: 14, weight: .semibold))
.font(.tjScaled( 14, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("\(reports.count) 份 · \(indicators.count) 项指标 · 端侧加密")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)

View File

@@ -34,12 +34,12 @@ struct RecentItemRow: View {
VStack(alignment: .leading, spacing: 2) {
Text("\(date) · \(type)")
.font(.system(size: 11))
.font(.tjScaled( 11))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
Text(name)
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
.truncationMode(.tail)
@@ -47,7 +47,7 @@ struct RecentItemRow: View {
Spacer(minLength: 8)
if let value {
Text(value)
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.font(.tjScaled( 12, weight: .semibold, design: .monospaced))
.foregroundStyle(status.valueColor)
.lineLimit(1)
.fixedSize()

View File

@@ -61,12 +61,12 @@ struct TodayRemindersCard: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("\(count)")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
Button { showingCenter = true } label: {
Text("全部 ")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
.buttonStyle(.plain)
@@ -77,14 +77,14 @@ struct TodayRemindersCard: View {
let isPast = item.isPast(now: tick)
return HStack(spacing: 12) {
Text(item.timeLabel)
.font(.system(size: 14, weight: .semibold).monospacedDigit())
.font(.tjScaled( 14, weight: .semibold).monospacedDigit())
.foregroundStyle(isPast ? Tj.Palette.text3 : Tj.Palette.ink)
.frame(width: 46, alignment: .leading)
Image(systemName: "bell.fill")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(isPast ? Tj.Palette.text3 : Tj.Palette.amber)
Text(item.title)
.font(.system(size: 15, weight: .medium))
.font(.tjScaled( 15, weight: .medium))
.foregroundStyle(isPast ? Tj.Palette.text3 : Tj.Palette.text)
.lineLimit(1)
Spacer(minLength: 0)

View File

@@ -125,7 +125,7 @@ struct CustomMetricEditor: View {
Spacer()
if existing == nil {
Text("保存后会出现在录入选项里")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -147,10 +147,10 @@ struct CustomMetricEditor: View {
if nameConflict != .none {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.amber)
Text(nameConflict.warningText)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.amber)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 0)
@@ -175,7 +175,7 @@ struct CustomMetricEditor: View {
sectionLabel(String(appLoc: "参考范围(可选)"))
Spacer()
Text("用于自动判定 正常/偏高/偏低")
.font(.system(size: 10))
.font(.tjScaled( 10))
.foregroundStyle(Tj.Palette.text3)
}
HStack(spacing: 12) {
@@ -188,10 +188,10 @@ struct CustomMetricEditor: View {
private func rangeField(label: String, value: Binding<String>, placeholder: String) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(label).font(.system(size: 11)).foregroundStyle(Tj.Palette.text3)
Text(label).font(.tjScaled( 11)).foregroundStyle(Tj.Palette.text3)
TextField(placeholder, text: value)
.keyboardType(.decimalPad)
.font(.system(size: 16, weight: .medium, design: .monospaced))
.font(.tjScaled( 16, weight: .medium, design: .monospaced))
.padding(.horizontal, 12).padding(.vertical, 10)
.background(fieldBg).overlay(fieldBorder)
}
@@ -207,7 +207,7 @@ struct CustomMetricEditor: View {
icon = sf
} label: {
Image(systemName: sf)
.font(.system(size: 20, weight: .medium))
.font(.tjScaled( 20, weight: .medium))
.foregroundStyle(icon == sf ? Tj.Palette.paper : Tj.Palette.ink)
.frame(maxWidth: .infinity, minHeight: 44)
.background(
@@ -239,7 +239,7 @@ struct CustomMetricEditor: View {
Image(systemName: "trash")
Text("删除这项自定义指标")
}
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
@@ -282,7 +282,7 @@ struct CustomMetricEditor: View {
.strokeBorder(Tj.Palette.line, lineWidth: 1)
}
private func sectionLabel(_ t: String) -> some View {
Text(t).font(.system(size: 12, weight: .semibold)).tracking(0.3)
Text(t).font(.tjScaled( 12, weight: .semibold)).tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
}

View File

@@ -27,6 +27,10 @@ private let labPresets: [IndicatorPreset] = [
/// seriesKey, Trends
/// 3. **** name/value/unit/range ,status
struct IndicatorQuickSheet: View {
/// RootView : QuickRegionCaptureFlow(VL)
/// nil ( Preview)
var onRequestCamera: (() -> Void)? = nil
@Environment(\.modelContext) private var ctx
@Environment(\.dismiss) private var dismiss
@Query private var profiles: [UserProfile]
@@ -103,6 +107,7 @@ struct IndicatorQuickSheet: View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 20) {
cameraEntrySection
monitorGridSection
labPresetSection
Divider().padding(.vertical, 4)
@@ -161,13 +166,69 @@ struct IndicatorQuickSheet: View {
.foregroundStyle(Tj.Palette.text)
Spacer()
Text("本地处理 · 永不上传")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.horizontal, 20)
.padding(.bottom, 16)
}
/// : RootView VL
@ViewBuilder
private var cameraEntrySection: some View {
if let onRequestCamera {
VStack(alignment: .leading, spacing: 10) {
Button {
onRequestCamera()
} label: {
HStack(spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(Tj.Palette.brick)
Image(systemName: "camera.fill")
.font(.tjScaled(18, weight: .medium))
.foregroundStyle(Tj.Palette.paper)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("拍照识别")
.font(.tjScaled(15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("拍化验单,VL 自动读出数值")
.font(.tjScaled(12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
Image(systemName: "chevron.right")
.font(.tjScaled(14, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
.frame(maxWidth: .infinity)
.tjCard(bordered: true)
}
.buttonStyle(.plain)
HStack(spacing: 8) {
line
Text("或手动填写")
.font(.tjScaled(11))
.foregroundStyle(Tj.Palette.text3)
.fixedSize()
line
}
}
}
}
private var line: some View {
Rectangle()
.fill(Tj.Palette.lineSoft)
.frame(height: 1)
.frame(maxWidth: .infinity)
}
private var monitorGridSection: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
@@ -217,18 +278,18 @@ struct IndicatorQuickSheet: View {
} label: {
HStack(spacing: 10) {
Image(systemName: cm.icon)
.font(.system(size: 18, weight: .medium))
.font(.tjScaled( 18, weight: .medium))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.ink)
.frame(width: 32, height: 32)
.background(Circle().fill(selected ? Tj.Palette.ink : Tj.Palette.leafSoft))
VStack(alignment: .leading, spacing: 1) {
Text(cm.name)
.font(.system(size: 14, weight: selected ? .semibold : .medium))
.font(.tjScaled( 14, weight: selected ? .semibold : .medium))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text)
.lineLimit(1)
Text("自定义")
.font(.system(size: 9, design: .monospaced))
.font(.tjScaled( 9, design: .monospaced))
.foregroundStyle(selected ? Tj.Palette.paper.opacity(0.7) : Tj.Palette.text3)
}
Spacer()
@@ -260,14 +321,14 @@ struct IndicatorQuickSheet: View {
} label: {
HStack(spacing: 10) {
Image(systemName: "plus")
.font(.system(size: 18, weight: .semibold))
.font(.tjScaled( 18, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
.frame(width: 32, height: 32)
.background(
Circle().strokeBorder(Tj.Palette.line, lineWidth: 1, antialiased: true)
)
Text("自定义")
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text2)
Spacer()
}
@@ -293,13 +354,13 @@ struct IndicatorQuickSheet: View {
} label: {
HStack(spacing: 10) {
Image(systemName: m.icon)
.font(.system(size: 18, weight: .medium))
.font(.tjScaled( 18, weight: .medium))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.ink)
.frame(width: 32, height: 32)
.background(Circle().fill(selected ? Tj.Palette.ink : Tj.Palette.amber.opacity(0.25)))
Text(m.displayName)
.font(.system(size: 14, weight: selected ? .semibold : .medium))
.font(.tjScaled( 14, weight: selected ? .semibold : .medium))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text)
Spacer()
}
@@ -348,7 +409,7 @@ struct IndicatorQuickSheet: View {
}
HStack(spacing: 12) {
bpField(label: String(appLoc: "收缩压"), value: $systolic, placeholder: "120")
Text("/").font(.system(size: 22, weight: .light)).foregroundStyle(Tj.Palette.text3)
Text("/").font(.tjScaled( 22, weight: .light)).foregroundStyle(Tj.Palette.text3)
bpField(label: String(appLoc: "舒张压"), value: $diastolic, placeholder: "80")
Text("mmHg").foregroundStyle(Tj.Palette.text3)
}
@@ -358,10 +419,10 @@ struct IndicatorQuickSheet: View {
private func bpField(label: String, value: Binding<String>, placeholder: String) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(label).font(.system(size: 11)).foregroundStyle(Tj.Palette.text3)
Text(label).font(.tjScaled( 11)).foregroundStyle(Tj.Palette.text3)
TextField(placeholder, text: value)
.keyboardType(.decimalPad)
.font(.system(size: 20, weight: .semibold, design: .monospaced))
.font(.tjScaled( 20, weight: .semibold, design: .monospaced))
.multilineTextAlignment(.center)
.padding(.vertical, 10)
.frame(width: 90)
@@ -380,11 +441,11 @@ struct IndicatorQuickSheet: View {
let rangeText = "\(formatRange(sysRange)) / \(formatRange(diasRange))"
return HStack(spacing: 4) {
Text(rangeText)
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
if personalized, let age = profile?.age {
Text("· 按\(age)岁调整")
.font(.system(size: 10))
.font(.tjScaled( 10))
.foregroundStyle(Tj.Palette.amber)
}
}
@@ -427,7 +488,7 @@ struct IndicatorQuickSheet: View {
sectionLabel(String(appLoc: "数值"))
TextField(monitorFieldPlaceholder, text: $value)
.keyboardType(.decimalPad)
.font(.system(size: 18, weight: .semibold, design: .monospaced))
.font(.tjScaled( 18, weight: .semibold, design: .monospaced))
.padding(.horizontal, 14)
.padding(.vertical, 12)
.background(fieldBg)
@@ -475,7 +536,7 @@ struct IndicatorQuickSheet: View {
return HStack(spacing: 4) {
if personalized, let age = profile?.age {
Text("\(age)岁调整")
.font(.system(size: 10))
.font(.tjScaled( 10))
.foregroundStyle(Tj.Palette.amber)
}
}
@@ -500,7 +561,7 @@ struct IndicatorQuickSheet: View {
statusBadge(s.label, color: s.color)
} else {
Text("待输入")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -546,7 +607,7 @@ struct IndicatorQuickSheet: View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("时间")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
Spacer()
DatePicker("", selection: $reminderTime,
@@ -558,11 +619,11 @@ struct IndicatorQuickSheet: View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("频率")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
Spacer()
Text(reminderFrequencyLabel)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
weekdayPickerRow
@@ -581,11 +642,11 @@ struct IndicatorQuickSheet: View {
if notifAuthBlocked {
Text("⚠️ 通知权限已关闭,去「设置 → 康康 → 通知」打开")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.brick)
} else {
Text("本机提醒 · 不发任何数据")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -625,7 +686,7 @@ struct IndicatorQuickSheet: View {
}
} label: {
Text(names[idx])
.font(.system(size: 13,
.font(.tjScaled( 13,
weight: reminderWeekdays.contains(w) ? .semibold : .regular))
.foregroundStyle(reminderWeekdays.contains(w) ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity, minHeight: 32)
@@ -647,7 +708,7 @@ struct IndicatorQuickSheet: View {
private func quickFreqChip(_ label: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(label)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text2)
.padding(.horizontal, 10)
.padding(.vertical, 4)
@@ -755,7 +816,7 @@ struct IndicatorQuickSheet: View {
private func sectionLabel(_ text: String) -> some View {
Text(text)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
}
@@ -763,7 +824,7 @@ struct IndicatorQuickSheet: View {
private func chip(_ label: String, selected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(label)
.font(.system(size: 13, weight: selected ? .semibold : .regular))
.font(.tjScaled( 13, weight: selected ? .semibold : .regular))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text)
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -779,7 +840,7 @@ struct IndicatorQuickSheet: View {
manualStatus = value
} label: {
Text(label)
.font(.system(size: 13, weight: selected ? .semibold : .regular))
.font(.tjScaled( 13, weight: selected ? .semibold : .regular))
.foregroundStyle(selected ? Tj.Palette.paper : color)
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -792,7 +853,7 @@ struct IndicatorQuickSheet: View {
private func statusBadge(_ label: String, color: Color) -> some View {
Text(label)
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.foregroundStyle(color)
.padding(.horizontal, 10)
.padding(.vertical, 4)
@@ -832,9 +893,9 @@ struct IndicatorQuickSheet: View {
} label: {
HStack(spacing: 3) {
Text("已隐藏 \(hiddenSet.count)")
.font(.system(size: 11, weight: .medium))
.font(.tjScaled( 11, weight: .medium))
Image(systemName: "chevron.right")
.font(.system(size: 9, weight: .semibold))
.font(.tjScaled( 9, weight: .semibold))
}
.foregroundStyle(Tj.Palette.text2)
.padding(.horizontal, 10)
@@ -1121,7 +1182,7 @@ private struct HiddenMonitorRestoreSheet: View {
.foregroundStyle(Tj.Palette.text)
Spacer()
Button("完成") { dismiss() }
.font(.system(size: 14))
.font(.tjScaled( 14))
.foregroundStyle(Tj.Palette.ink)
}
.padding(.horizontal, 20)
@@ -1146,13 +1207,13 @@ private struct HiddenMonitorRestoreSheet: View {
private func row(_ m: MonitorMetric) -> some View {
HStack(spacing: 12) {
Image(systemName: m.icon)
.font(.system(size: 16, weight: .medium))
.font(.tjScaled( 16, weight: .medium))
.foregroundStyle(Tj.Palette.ink)
.frame(width: 32, height: 32)
.background(Circle().fill(Tj.Palette.amber.opacity(0.25)))
Text(m.displayName)
.font(.system(size: 15, weight: .medium))
.font(.tjScaled( 15, weight: .medium))
.foregroundStyle(Tj.Palette.text)
Spacer()
@@ -1160,7 +1221,7 @@ private struct HiddenMonitorRestoreSheet: View {
Button("显示") {
onRestore(m)
}
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, 14)
.padding(.vertical, 6)

View File

@@ -70,12 +70,12 @@ struct AboutView: View {
}
Text("康康 · 本地优先的健康档案 · \(versionText)")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.padding(.top, 4)
Text("本 App 仅供健康信息记录与参考,不能替代专业医疗意见。")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
@@ -98,7 +98,7 @@ struct AboutView: View {
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.fill(Tj.Palette.sand2)
Image(systemName: "heart.text.square.fill")
.font(.system(size: 34))
.font(.tjScaled( 34))
.foregroundStyle(Tj.Palette.brick)
}
.frame(width: 72, height: 72)
@@ -108,7 +108,7 @@ struct AboutView: View {
.foregroundStyle(Tj.Palette.text)
Text("本地优先的个人健康随记")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
Text(versionText)
@@ -133,10 +133,10 @@ struct AboutView: View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(tint)
Text(title)
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
}
content()
@@ -148,7 +148,7 @@ struct AboutView: View {
@ViewBuilder private func paragraph(_ text: String) -> some View {
Text(text)
.font(.system(size: 14))
.font(.tjScaled( 14))
.foregroundStyle(Tj.Palette.text2)
.lineSpacing(5)
.fixedSize(horizontal: false, vertical: true)
@@ -161,7 +161,7 @@ struct AboutView: View {
.frame(width: 5, height: 5)
.padding(.top, 7)
Text(text)
.font(.system(size: 14))
.font(.tjScaled( 14))
.foregroundStyle(Tj.Palette.text2)
.lineSpacing(5)
.fixedSize(horizontal: false, vertical: true)

View File

@@ -41,7 +41,7 @@ struct CustomMetricsListView: View {
editingTarget = CustomMetricEditTarget(metric: nil)
} label: {
Image(systemName: "plus")
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
}
}
}
@@ -57,7 +57,7 @@ struct CustomMetricsListView: View {
Image(systemName: "info.circle.fill")
.foregroundStyle(Tj.Palette.text3)
Text("自定义指标会出现在「+ 指标记录 → 长期监测」的 grid 里,可设提醒、进趋势")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text2)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 0)
@@ -75,7 +75,7 @@ struct CustomMetricsListView: View {
TjPlaceholder(label: String(appLoc: "还没有自定义指标"))
.frame(width: 220, height: 130)
Text("右上角 + 新建一个")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
@@ -88,28 +88,28 @@ struct CustomMetricsListView: View {
ZStack {
Circle().fill(Tj.Palette.leafSoft)
Image(systemName: m.icon)
.font(.system(size: 17, weight: .medium))
.font(.tjScaled( 17, weight: .medium))
.foregroundStyle(Tj.Palette.ink)
}
.frame(width: 40, height: 40)
VStack(alignment: .leading, spacing: 3) {
Text(m.name)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
HStack(spacing: 6) {
if !m.unit.isEmpty {
Text(m.unit)
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
if !m.rangeText.isEmpty {
Text("·")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
Text(m.rangeText)
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -119,10 +119,10 @@ struct CustomMetricsListView: View {
VStack(alignment: .trailing, spacing: 2) {
Text(count == 0 ? String(appLoc: "未使用") : String(appLoc: "\(count)"))
.font(.system(size: 11, weight: count > 0 ? .semibold : .regular))
.font(.tjScaled( 11, weight: count > 0 ? .semibold : .regular))
.foregroundStyle(count > 0 ? Tj.Palette.ink : Tj.Palette.text3)
Image(systemName: "chevron.right")
.font(.system(size: 11, weight: .medium))
.font(.tjScaled( 11, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
}

View File

@@ -142,7 +142,7 @@ struct CustomReminderEditSheet: View {
private var skipHint: some View {
Text(String(appLoc: "部分月份无此日,该月将跳过"))
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
@@ -169,7 +169,7 @@ struct CustomReminderEditSheet: View {
second: 0, of: pickedTime) ?? pickedTime
} label: {
Text(String(format: "%d:%02d", preset.h, preset.m))
.font(.system(size: 13, weight: on ? .semibold : .regular))
.font(.tjScaled( 13, weight: on ? .semibold : .regular))
.foregroundStyle(on ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity, minHeight: 30)
.background(
@@ -203,7 +203,7 @@ struct CustomReminderEditSheet: View {
if on { weekdays.remove(w) } else { weekdays.insert(w) }
} label: {
Text(names[idx])
.font(.system(size: 13, weight: on ? .semibold : .regular))
.font(.tjScaled( 13, weight: on ? .semibold : .regular))
.foregroundStyle(on ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity, minHeight: 30)
.background(

View File

@@ -0,0 +1,81 @@
import SwiftUI
/// · / : App
/// (,,)
/// ,便
struct FontSettingsView: View {
@State private var manager = FontScaleManager.shared
/// (,,)
private let sampleBase: CGFloat = 17
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(FontScale.allCases) { option in
row(option)
}
Text("放大后整个 App 的文字立即变大,无需重启。设置会被记住。")
.font(.tjScaled(12))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 4)
.padding(.top, 6)
}
.padding(.horizontal, 16)
.padding(.vertical, 20)
}
.background(Tj.Palette.sand.ignoresSafeArea())
.navigationTitle("字体大小")
.navigationBarTitleDisplayMode(.inline)
}
private func row(_ option: FontScale) -> some View {
let selected = manager.scale == option
return Button {
manager.set(option)
} label: {
HStack(spacing: 14) {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Text(option.label)
.font(.system(size: 15, weight: selected ? .semibold : .regular))
.foregroundStyle(Tj.Palette.text)
Text(option.detail)
.font(.system(size: 11))
.foregroundStyle(Tj.Palette.text3)
}
// :,
Text("健康档案 Aa 123")
.font(.system(size: sampleBase * option.multiplier, weight: .medium))
.foregroundStyle(Tj.Palette.text2)
.lineLimit(1)
.minimumScaleFactor(0.5)
}
Spacer(minLength: 8)
ZStack {
Circle()
.strokeBorder(selected ? Tj.Palette.ink : Tj.Palette.line, lineWidth: selected ? 0 : 1.5)
.background(Circle().fill(selected ? Tj.Palette.ink : Color.clear))
.frame(width: 24, height: 24)
if selected {
Image(systemName: "checkmark")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(Tj.Palette.paper)
}
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.tjCard()
}
.buttonStyle(.plain)
}
}
#Preview {
NavigationStack { FontSettingsView() }
}

View File

@@ -12,7 +12,7 @@ struct LanguageSettingsView: View {
}
Text("切换后整个 App 立即生效,无需重启。")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 4)
@@ -40,14 +40,14 @@ struct LanguageSettingsView: View {
.frame(width: 40, height: 40)
Text(option.displayName)
.font(.system(size: 15, weight: selected ? .semibold : .regular))
.font(.tjScaled( 15, weight: selected ? .semibold : .regular))
.foregroundStyle(Tj.Palette.text)
Spacer()
if selected {
Image(systemName: "checkmark")
.font(.system(size: 14, weight: .semibold))
.font(.tjScaled( 14, weight: .semibold))
.foregroundStyle(Tj.Palette.ink)
}
}
@@ -64,11 +64,11 @@ struct LanguageSettingsView: View {
switch option.pickerIcon {
case .symbol(let name):
Image(systemName: name)
.font(.system(size: 16))
.font(.tjScaled( 16))
.foregroundStyle(fg)
case .glyph(let g):
Text(verbatim: g)
.font(.system(size: 17, weight: .semibold))
.font(.tjScaled( 17, weight: .semibold))
.foregroundStyle(fg)
}
}

View File

@@ -9,6 +9,7 @@ struct MeView: View {
@State private var downloadService = ModelDownloadService.shared
@State private var appLock = AppLock.shared
@State private var lang = LanguageManager.shared
@State private var fontScale = FontScaleManager.shared
// key AppLock.enabledKey
@AppStorage("faceIDLockEnabled") private var lockEnabled = false
@@ -37,6 +38,7 @@ struct MeView: View {
customMetricsCard
modelManagementCard
languageCard
fontScaleCard
faceIDCard
NavigationLink {
AboutView()
@@ -74,23 +76,23 @@ struct MeView: View {
Circle()
.fill(Tj.Palette.amber.opacity(0.25))
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 22))
.font(.tjScaled( 22))
.foregroundStyle(Tj.Palette.ink)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("个人资料")
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text(profileLine)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
@@ -108,23 +110,23 @@ struct MeView: View {
Circle()
.fill(customMetrics.isEmpty ? Tj.Palette.sand2 : Tj.Palette.leafSoft)
Image(systemName: "slider.horizontal.3")
.font(.system(size: 18))
.font(.tjScaled( 18))
.foregroundStyle(customMetrics.isEmpty ? Tj.Palette.text2 : Tj.Palette.ink)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("自定义指标")
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text(customMetricsLine)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)
@@ -166,6 +168,17 @@ struct MeView: View {
.buttonStyle(.plain)
}
private var fontScaleCard: some View {
NavigationLink {
FontSettingsView()
} label: {
settingsCard(title: String(appLoc: "字体大小"),
detail: fontScale.scale.label,
icon: "textformat.size")
}
.buttonStyle(.plain)
}
// MARK: - Face ID ( Toggle )
private var faceIDCard: some View {
@@ -173,17 +186,17 @@ struct MeView: View {
ZStack {
Circle().fill(lockEnabled ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "faceid")
.font(.system(size: 18))
.font(.tjScaled( 18))
.foregroundStyle(lockEnabled ? Tj.Palette.ink : Tj.Palette.text2)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("Face ID 启动锁")
.font(.system(size: 15, weight: .medium))
.font(.tjScaled( 15, weight: .medium))
.foregroundStyle(Tj.Palette.text)
Text(faceIDLine)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
@@ -219,20 +232,20 @@ struct MeView: View {
ZStack {
Circle().fill(Tj.Palette.sand2)
Image(systemName: icon)
.font(.system(size: 18))
.font(.tjScaled( 18))
.foregroundStyle(Tj.Palette.text2)
}
.frame(width: 44, height: 44)
Text(title)
.font(.system(size: 15, weight: .medium))
.font(.tjScaled( 15, weight: .medium))
.foregroundStyle(Tj.Palette.text)
Spacer()
Text(detail)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Image(systemName: "chevron.right")
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)

View File

@@ -43,7 +43,7 @@ struct ModelManagementView: View {
if let importError {
Text(importError)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.brick)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -86,10 +86,10 @@ struct ModelManagementView: View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 3) {
Text(kind.displayName)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text(subtitle(kind))
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
@@ -104,17 +104,17 @@ struct ModelManagementView: View {
Spacer()
Text(speedText(state))
}
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
} else {
HStack {
Text(formatBytes(ModelManifest.totalBytes(for: kind)))
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
Spacer()
if case .failed(let message) = state.phase {
Text(message)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.brick)
.lineLimit(1)
}
@@ -156,7 +156,7 @@ struct ModelManagementView: View {
Image(systemName: "checkmark.seal.fill")
Text("两个模型都已就绪")
}
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.leaf)
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
@@ -183,7 +183,7 @@ struct ModelManagementView: View {
VStack(spacing: 8) {
TjLockChip()
Text("100% 本地推理 · 模型仅需下载一次")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
.frame(maxWidth: .infinity)

View File

@@ -37,11 +37,11 @@ struct ModelSelfTestView: View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
Text("测试 PROMPT")
.font(.system(size: 11, weight: .semibold))
.font(.tjScaled( 11, weight: .semibold))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text3)
Text(prompt)
.font(.system(size: 14))
.font(.tjScaled( 14))
.foregroundStyle(Tj.Palette.text)
}
.padding(14)
@@ -50,13 +50,13 @@ struct ModelSelfTestView: View {
HStack {
Text(phase.label)
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(statusColor)
.lineLimit(1)
Spacer()
if rate > 0 {
Text(String(format: "%.1f tok/s", rate))
.font(.system(size: 12, design: .monospaced))
.font(.tjScaled( 12, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}

View File

@@ -74,7 +74,7 @@ struct RemindersListView: View {
private var header: some View {
Text("新建提醒,或在记录指标时开启")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -89,7 +89,7 @@ struct RemindersListView: View {
private func sectionLabel(_ text: String) -> some View {
Text(text)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 8)
@@ -146,18 +146,18 @@ private struct CustomReminderRow: View {
Circle()
.fill(reminder.enabled ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "bell.fill")
.font(.system(size: 16))
.font(.tjScaled( 16))
.foregroundStyle(reminder.enabled ? Tj.Palette.ink : Tj.Palette.text3)
}
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
Text(reminder.title)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
Text("\(reminder.timeLabel) · \(reminder.frequencyLabel)")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer(minLength: 0)
@@ -173,7 +173,7 @@ private struct CustomReminderRow: View {
// 28×28 , Toggle
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.frame(width: 28, height: 28)
}
@@ -223,17 +223,17 @@ private struct ReminderRow: View {
Circle()
.fill(reminder.enabled ? Tj.Palette.amber.opacity(0.25) : Tj.Palette.sand2)
Image(systemName: "bell.fill")
.font(.system(size: 16))
.font(.tjScaled( 16))
.foregroundStyle(reminder.enabled ? Tj.Palette.ink : Tj.Palette.text3)
}
.frame(width: 36, height: 36)
VStack(alignment: .leading, spacing: 2) {
Text(reminder.displayName)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("\(reminder.timeLabel) · \(reminder.frequencyLabel)")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
@@ -248,7 +248,7 @@ private struct ReminderRow: View {
onTapEdit()
} label: {
Image(systemName: isEditing ? "chevron.up" : "chevron.down")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.frame(width: 28, height: 28)
}
@@ -259,7 +259,7 @@ private struct ReminderRow: View {
private var editingPanel: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("时间").font(.system(size: 13)).foregroundStyle(Tj.Palette.text2)
Text("时间").font(.tjScaled( 13)).foregroundStyle(Tj.Palette.text2)
Spacer()
DatePicker("", selection: $pickedTime, displayedComponents: .hourAndMinute)
.datePickerStyle(.compact)
@@ -278,7 +278,7 @@ private struct ReminderRow: View {
onDelete()
} label: {
Label("删除提醒", systemImage: "trash")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
}
.buttonStyle(.plain)
@@ -310,7 +310,7 @@ private struct ReminderRow: View {
onChange()
} label: {
Text(names[idx])
.font(.system(size: 13,
.font(.tjScaled( 13,
weight: reminder.weekdays.contains(w) ? .semibold : .regular))
.foregroundStyle(reminder.weekdays.contains(w) ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity, minHeight: 30)

View File

@@ -35,9 +35,40 @@ struct ProfileEditView: View {
private struct ProfileEditForm: View {
@Environment(\.modelContext) private var ctx
@Bindable var profile: UserProfile
@State private var healthImportDraft: HealthProfileImportDraft?
@State private var healthImportError: String?
@State private var isImportingHealthProfile = false
var body: some View {
Form {
Section {
Button {
importHealthProfile()
} label: {
HStack(spacing: 10) {
if isImportingHealthProfile {
ProgressView()
} else {
Image(systemName: "heart.text.square")
.foregroundStyle(Tj.Palette.ink)
}
VStack(alignment: .leading, spacing: 2) {
Text("从 Apple 健康导入")
.foregroundStyle(Tj.Palette.text)
Text("只读取生日、性别、身高、血型")
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
}
}
.disabled(isImportingHealthProfile)
.accessibilityElement(children: .combine)
.accessibilityLabel("从 Apple 健康导入")
.accessibilityHint("读取生日、性别、身高和血型,确认后填入个人资料")
} footer: {
Text("导入前会先显示预览,确认后才覆盖个人资料。")
}
Section {
BirthYearRow(profile: profile)
SexRow(profile: profile)
@@ -67,6 +98,90 @@ private struct ProfileEditForm: View {
profile.updatedAt = .now
try? ctx.save()
}
.sheet(item: $healthImportDraft) { draft in
HealthProfileImportPreviewSheet(
draft: draft,
profile: profile
) {
draft.apply(to: profile)
try? ctx.save()
healthImportDraft = nil
}
}
.alert("无法导入 Apple 健康资料", isPresented: Binding(
get: { healthImportError != nil },
set: { if !$0 { healthImportError = nil } }
)) {
Button("", role: .cancel) { healthImportError = nil }
} message: {
Text(healthImportError ?? "")
}
}
private func importHealthProfile() {
guard !isImportingHealthProfile else { return }
isImportingHealthProfile = true
healthImportError = nil
Task {
do {
healthImportDraft = try await HealthProfileImportService.shared.fetchDraft()
} catch {
healthImportError = error.localizedDescription
}
isImportingHealthProfile = false
}
}
}
private struct HealthProfileImportPreviewSheet: View {
@Environment(\.dismiss) private var dismiss
let draft: HealthProfileImportDraft
let profile: UserProfile
let onApply: () -> Void
private var preview: HealthProfileImportPreview {
HealthProfileImportPreview(draft: draft, current: profile)
}
var body: some View {
NavigationStack {
List {
Section {
ForEach(preview.fields, id: \.title) { field in
HStack(alignment: .firstTextBaseline) {
Text(field.title)
.foregroundStyle(Tj.Palette.text)
Spacer(minLength: 12)
VStack(alignment: .trailing, spacing: 4) {
Text(field.imported ?? "未读取到")
.foregroundStyle(field.imported == nil ? Tj.Palette.text3 : Tj.Palette.text)
Text("当前: \(field.current)")
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
}
}
} footer: {
Text("未读取到的字段不会修改。")
}
}
.navigationTitle("确认导入")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(Tj.Palette.sand.ignoresSafeArea())
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("导入") {
onApply()
dismiss()
}
.fontWeight(.semibold)
}
}
}
}
}
@@ -112,7 +227,7 @@ private struct BirthYearRow: View {
Text(selectedLabel)
.foregroundStyle(profile.birthYear == nil ? Tj.Palette.text3 : Tj.Palette.text2)
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text3)
.rotationEffect(.degrees(expanded ? 90 : 0))
}
@@ -212,7 +327,7 @@ private struct BMIFooter: View {
var body: some View {
if let bmi = profile.bmi {
Text("BMI: \(String(format: "%.1f", bmi)) \(label(bmi))")
.font(.system(size: 11))
.font(.tjScaled( 11))
}
}
@@ -282,7 +397,7 @@ private struct ChronicSection: View {
private func chip(label: String, selected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(label)
.font(.system(size: 13, weight: selected ? .semibold : .regular))
.font(.tjScaled( 13, weight: selected ? .semibold : .regular))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text)
.padding(.horizontal, 12)
.padding(.vertical, 6)

View File

@@ -21,7 +21,8 @@ struct QuickRegionCaptureFlow: View {
@State private var analyzeTask: Task<Void, Never>? = nil
/// VL (); cancel ,UI
private let analyzeTimeoutSeconds: Int = 30
/// token ,30s , 60s
private let analyzeTimeoutSeconds: Int = 60
enum Phase {
case idle
@@ -86,23 +87,42 @@ struct QuickRegionCaptureFlow: View {
}
}
// MARK: - :()/ ()
// MARK: - :()/ ()
// RegionCameraView ( 1-2 ); ·
// , ,VL VisionKit :
// + ,VL / 退
@ViewBuilder
private var captureEntry: some View {
#if targetEnvironment(simulator)
PhotoPickerSheet(
onFinish: { imgs in if let first = imgs.first { startAnalyze(image: first) } },
onFinish: { imgs in handleScanned(imgs) },
onCancel: onClose
)
#else
RegionCameraView(
onCapture: { startAnalyze(image: $0) },
onCancel: onClose
)
if DocumentScannerView.isSupported {
DocumentScannerView(
onFinish: { imgs in handleScanned(imgs) },
onCancel: onClose
)
} else {
PhotoPickerSheet(
onFinish: { imgs in handleScanned(imgs) },
onCancel: onClose
)
}
#endif
}
/// /:();
private func handleScanned(_ images: [UIImage]) {
if let first = images.first {
startAnalyze(image: first)
} else {
onClose()
}
}
// MARK: -
private func startAnalyze(image: UIImage) {
@@ -110,12 +130,9 @@ struct QuickRegionCaptureFlow: View {
phase = .analyzing(image: image)
let timeout = analyzeTimeoutSeconds
// MainActor ,Task{} , phase 线,
// :Vision OCR Qwen3-1.7B LLM ( 3B VL )
analyzeTask = Task {
guard let data = image.jpegData(compressionQuality: 0.9) else {
phase = .confirm(image: image, items: [],
warning: String(appLoc: "图片编码失败,手动补充或重拍"))
return
}
let timeoutWarn = String(appLoc: "识别超时(>\(timeout)s),手动补充或重拍")
let watchdog = Task {
try? await Task.sleep(for: .seconds(timeout))
@@ -124,12 +141,25 @@ struct QuickRegionCaptureFlow: View {
defer { watchdog.cancel() }
do {
let parsed = try await CaptureService.shared.recognizeRegion(imageData: data)
// 1. OCR
let text = try await OCRService.recognizeText(in: image)
if Task.isCancelled {
phase = .confirm(image: image, items: [], warning: timeoutWarn); return
}
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
#if DEBUG
print("🔤 [OCR] recognized text:\n\(trimmed)\n--- end OCR ---")
#endif
if trimmed.isEmpty {
phase = .confirm(image: image, items: [],
warning: String(appLoc: "识别超时(>\(timeout)s),手动补充或重拍"))
warning: String(appLoc: "识别到文字,手动补充或重拍"))
return
}
// 2. LLM
let parsed = try await CaptureService.shared.recognizeIndicators(fromOCRText: trimmed)
if Task.isCancelled {
phase = .confirm(image: image, items: [], warning: timeoutWarn); return
}
let items = Self.buildItems(from: parsed)
phase = .confirm(
image: image,
@@ -138,23 +168,23 @@ struct QuickRegionCaptureFlow: View {
)
} catch CaptureError.modelNotReady {
phase = .confirm(image: image, items: [],
warning: String(appLoc: "VL 模型未就绪,手动补充"))
warning: String(appLoc: "AI 模型未就绪,手动补充"))
} catch let CaptureError.parseFailed(msg) {
phase = .confirm(image: image, items: [],
warning: String(appLoc: "VL 输出无法解析:\(msg)"))
warning: String(appLoc: "解析失败:\(msg)"))
} catch let CaptureError.inferenceFailed(msg) {
phase = .confirm(image: image, items: [],
warning: Task.isCancelled
? String(appLoc: "识别超时(>\(timeout)s),手动补充或重拍")
: String(appLoc: "推理失败:\(msg)"))
warning: Task.isCancelled ? timeoutWarn
: String(appLoc: "识别失败:\(msg)"))
} catch {
phase = .confirm(image: image, items: [],
warning: String(appLoc: "未知错误:\(error.localizedDescription)"))
warning: Task.isCancelled ? timeoutWarn
: String(appLoc: "未知错误:\(error.localizedDescription)"))
}
}
}
/// VL ,(high/low)
/// LLM ,(high/low)
private static func buildItems(from parsed: [ParsedReport.ParsedIndicator]) -> [QuickRegionItem] {
let mapped = parsed.map {
QuickRegionItem(name: $0.name, value: $0.value, unit: $0.unit,
@@ -233,16 +263,16 @@ private struct AnalyzingRegionView: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("100% 本地推理 · 已用 \(elapsed)s")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
if elapsed >= timeoutSeconds - 5 {
Text("快超时了,>\(timeoutSeconds)s 会自动转手动录入")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.amber)
}
}
Button("取消识别 · 改为手动录入", action: onCancel)
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
.padding(.top, 4)
Spacer()

View File

@@ -55,7 +55,7 @@ struct QuickRegionConfirmView: View {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(Tj.Palette.amber)
Text(text)
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
Spacer()
}
@@ -70,11 +70,11 @@ struct QuickRegionConfirmView: View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("拍到的局部")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
Spacer()
Text("仅核对用 · 不保存照片")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
Image(uiImage: image)
@@ -91,7 +91,7 @@ struct QuickRegionConfirmView: View {
onRetake()
} label: {
Label("重拍", systemImage: "camera.rotate")
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.ink)
}
}
@@ -102,7 +102,7 @@ struct QuickRegionConfirmView: View {
private var timeCard: some View {
VStack(alignment: .leading, spacing: 10) {
Text("测量时间")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
DatePicker("", selection: $capturedAt, in: ...Date.now)
.datePickerStyle(.compact)
@@ -116,7 +116,7 @@ struct QuickRegionConfirmView: View {
VStack(alignment: .leading, spacing: 14) {
HStack {
Text("识别到的指标 (\(items.count))")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
Spacer()
Button {
@@ -124,14 +124,14 @@ struct QuickRegionConfirmView: View {
status: .high, include: true))
} label: {
Label("加一项", systemImage: "plus.circle.fill")
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(Tj.Palette.ink)
}
}
if items.isEmpty {
Text("没有识别到指标,点「加一项」手动补充,或返回重拍")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 20)
@@ -153,17 +153,17 @@ struct QuickRegionConfirmView: View {
item.wrappedValue.include.toggle()
} label: {
Image(systemName: item.wrappedValue.include ? "checkmark.circle.fill" : "circle")
.font(.system(size: 20))
.font(.tjScaled( 20))
.foregroundStyle(item.wrappedValue.include ? Tj.Palette.ink : Tj.Palette.text3)
}
.buttonStyle(.plain)
TextField(String(appLoc: "指标名"), text: item.name)
.font(.system(size: 15, weight: .medium))
.font(.tjScaled( 15, weight: .medium))
if abnormal {
Text(statusLabel(item.wrappedValue.status))
.font(.system(size: 10, weight: .semibold))
.font(.tjScaled( 10, weight: .semibold))
.foregroundStyle(statusColor(item.wrappedValue.status))
.padding(.horizontal, 7).padding(.vertical, 3)
.background(Capsule().fill(statusColor(item.wrappedValue.status).opacity(0.16)))
@@ -175,7 +175,7 @@ struct QuickRegionConfirmView: View {
}
} label: {
Image(systemName: "trash")
.font(.system(size: 14))
.font(.tjScaled( 14))
.foregroundStyle(Tj.Palette.brick)
}
}
@@ -203,10 +203,10 @@ struct QuickRegionConfirmView: View {
mono: Bool = false) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(label)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
TextField("", text: text)
.font(.system(size: 14, weight: mono ? .semibold : .regular,
.font(.tjScaled( 14, weight: mono ? .semibold : .regular,
design: mono ? .monospaced : .default))
.keyboardType(mono ? .decimalPad : .default)
.textInputAutocapitalization(.never)
@@ -234,7 +234,7 @@ struct QuickRegionConfirmView: View {
item.wrappedValue.status = st
} label: {
Text(statusLabel(st))
.font(.system(size: 12, weight: selected ? .semibold : .regular))
.font(.tjScaled( 12, weight: selected ? .semibold : .regular))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text2)
.padding(.horizontal, 12)
.padding(.vertical, 6)

View File

@@ -69,7 +69,7 @@ struct RegionCameraView: View {
//
Text("把异常项放进框里 · 对准一两行")
.font(.system(size: 13, weight: .medium))
.font(.tjScaled( 13, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
@@ -89,7 +89,7 @@ struct RegionCameraView: View {
onCancel()
} label: {
Text("取消")
.font(.system(size: 16, weight: .medium))
.font(.tjScaled( 16, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -126,19 +126,19 @@ struct RegionCameraView: View {
private var deniedView: some View {
VStack(spacing: 16) {
Image(systemName: "camera.fill")
.font(.system(size: 40))
.font(.tjScaled( 40))
.foregroundStyle(.white.opacity(0.8))
Text("相机权限未开启")
.font(.tjH2())
.foregroundStyle(.white)
Text("异常项快拍需要相机。去「设置 → 康康 → 相机」打开后再回来。")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(.white.opacity(0.7))
.multilineTextAlignment(.center)
.padding(.horizontal, 36)
HStack(spacing: 12) {
Button("取消") { onCancel() }
.font(.system(size: 15))
.font(.tjScaled( 15))
.foregroundStyle(.white)
.padding(.horizontal, 18).padding(.vertical, 10)
.background(Capsule().strokeBorder(.white.opacity(0.5), lineWidth: 1))
@@ -147,7 +147,7 @@ struct RegionCameraView: View {
UIApplication.shared.open(url)
}
}
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(.black)
.padding(.horizontal, 18).padding(.vertical, 10)
.background(Capsule().fill(.white))

View File

@@ -1,16 +1,18 @@
import SwiftUI
enum RecordKind: String, Identifiable, CaseIterable {
case quick, indicator, archive, diary, symptom, reminder
case quick, indicator, healthExport, archive, diary, symptom, reminder
var id: String { rawValue }
/// RecordSheet () enum ,
static let displayOrder: [RecordKind] = [.diary, .reminder, .symptom, .indicator, .quick, .archive]
/// :`.quick`() `.indicator`(),
static let displayOrder: [RecordKind] = [.diary, .reminder, .symptom, .indicator, .healthExport, .archive]
var title: String {
switch self {
case .quick: return String(appLoc: "异常项快拍")
case .indicator: return String(appLoc: "记录指标")
case .healthExport: return String(appLoc: "身体档案")
case .archive: return String(appLoc: "体检报告归档")
case .diary: return String(appLoc: "健康日记")
case .symptom: return String(appLoc: "记录症状")
@@ -20,7 +22,8 @@ enum RecordKind: String, Identifiable, CaseIterable {
var subtitle: String {
switch self {
case .quick: return String(appLoc: "拍一张化验单,VL 自动识别")
case .indicator: return String(appLoc: "手动填一项指标(免拍照)")
case .indicator: return String(appLoc: "手动填写,或拍照自动识别")
case .healthExport: return String(appLoc: "多轮问答后生成给医生看的整理报告")
case .archive: return String(appLoc: "完整保存整份报告(可多页)")
case .diary: return String(appLoc: "记录身体状态、用药、感受 · 可让 AI 辅助")
case .symptom: return String(appLoc: "开始一个持续症状,结束时再点结束")
@@ -31,6 +34,7 @@ enum RecordKind: String, Identifiable, CaseIterable {
switch self {
case .quick: return "camera.fill"
case .indicator: return "number.square.fill"
case .healthExport: return "doc.text.below.ecg"
case .archive: return "doc.fill"
case .diary: return "heart.text.square"
case .symptom: return "waveform.path.ecg"
@@ -41,6 +45,7 @@ enum RecordKind: String, Identifiable, CaseIterable {
switch self {
case .quick: return Tj.Palette.brick
case .indicator: return Tj.Palette.brick
case .healthExport: return Tj.Palette.ink
case .archive: return Tj.Palette.ink
case .diary: return Tj.Palette.leaf
case .symptom: return Tj.Palette.amber
@@ -66,7 +71,7 @@ struct RecordSheet: View {
.foregroundStyle(Tj.Palette.text)
Spacer()
Text("本地处理 · 永不上传")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.bottom, 14)
@@ -83,22 +88,22 @@ struct RecordSheet: View {
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
.fill(kind.accent)
Image(systemName: kind.icon)
.font(.system(size: 18, weight: .medium))
.font(.tjScaled( 18, weight: .medium))
.foregroundStyle(Tj.Palette.paper)
}
.frame(width: 44, height: 44)
VStack(alignment: .leading, spacing: 2) {
Text(kind.title)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text(kind.subtitle)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(16)

View File

@@ -25,7 +25,7 @@ struct OngoingSymptomsCard: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("\(ongoing.count)")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
@@ -51,12 +51,12 @@ struct OngoingSymptomsCard: View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Text(sym.name)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
severityDot(sym.severity)
}
Text("已持续 \(formatDuration(interval))")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(isLong ? Tj.Palette.brick : Tj.Palette.text3)
}
Spacer(minLength: 8)
@@ -64,7 +64,7 @@ struct OngoingSymptomsCard: View {
ending = sym
} label: {
Text("结束")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.padding(.horizontal, 12)
.padding(.vertical, 6)

View File

@@ -28,7 +28,7 @@ struct SymptomEndSheet: View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("结束症状")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text3)
Text(symptom.name)
@@ -40,16 +40,16 @@ struct SymptomEndSheet: View {
VStack(alignment: .leading, spacing: 6) {
Text("开始于")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Text(symptom.startedAt.formatted(date: .abbreviated, time: .shortened))
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text)
}
VStack(alignment: .leading, spacing: 8) {
Text("结束时间")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
DatePicker("", selection: $endedAt, in: lowerBound...Date.now)
@@ -59,11 +59,11 @@ struct SymptomEndSheet: View {
HStack {
Text("本次持续")
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text3)
Spacer()
Text(durationLabel)
.font(.system(size: 15, weight: .semibold, design: .monospaced))
.font(.tjScaled( 15, weight: .semibold, design: .monospaced))
.foregroundStyle(Tj.Palette.brick)
}
.padding(.horizontal, 14)

View File

@@ -69,7 +69,7 @@ struct SymptomStartSheet: View {
.foregroundStyle(Tj.Palette.text)
Spacer()
Text("结束时再来点结束")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.horizontal, 20)
@@ -130,15 +130,15 @@ struct SymptomStartSheet: View {
sectionLabel(String(appLoc: "强度"))
Spacer()
Text("\(Int(severity)) / 5")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.tjScaled( 13, weight: .semibold, design: .monospaced))
.foregroundStyle(severityColor)
}
Slider(value: $severity, in: 1...5, step: 1)
.tint(severityColor)
HStack {
Text("轻微").font(.system(size: 11)).foregroundStyle(Tj.Palette.text3)
Text("轻微").font(.tjScaled( 11)).foregroundStyle(Tj.Palette.text3)
Spacer()
Text("剧烈").font(.system(size: 11)).foregroundStyle(Tj.Palette.text3)
Text("剧烈").font(.tjScaled( 11)).foregroundStyle(Tj.Palette.text3)
}
}
}
@@ -190,7 +190,7 @@ struct SymptomStartSheet: View {
private func sectionLabel(_ text: String) -> some View {
Text(text)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
}
@@ -198,7 +198,7 @@ struct SymptomStartSheet: View {
private func chip(_ label: String, selected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(label)
.font(.system(size: 13, weight: selected ? .semibold : .regular))
.font(.tjScaled( 13, weight: selected ? .semibold : .regular))
.foregroundStyle(selected ? Tj.Palette.paper : Tj.Palette.text)
.padding(.horizontal, 14)
.padding(.vertical, 8)

View File

@@ -52,6 +52,7 @@ struct TimelineEntryDetailView: View {
let detail: TimelineDetail
@State private var showDeleteConfirm = false
@State private var evidenceTarget: Indicator?
var body: some View {
VStack(spacing: 0) {
@@ -77,6 +78,11 @@ struct TimelineEntryDetailView: View {
} message: {
Text("删除后无法恢复。")
}
.sheet(item: $evidenceTarget) { indicator in
if let report = indicator.report {
EvidenceImagePreview(report: report, indicator: indicator)
}
}
}
// MARK: - (:SwiftData + Vault unlink, CLAUDE.md §6)
@@ -84,7 +90,7 @@ struct TimelineEntryDetailView: View {
private var deleteButton: some View {
Button(role: .destructive) { showDeleteConfirm = true } label: {
Label(String(appLoc: "永久删除"), systemImage: "trash")
.font(.system(size: 12, weight: .medium))
.font(.tjScaled( 12, weight: .medium))
.foregroundStyle(Tj.Palette.brick.opacity(0.8))
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -136,7 +142,7 @@ struct TimelineEntryDetailView: View {
HStack(spacing: 12) {
Button { dismiss() } label: {
Image(systemName: "xmark")
.font(.system(size: 16, weight: .semibold))
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.frame(width: 32, height: 32)
.background(Circle().fill(Tj.Palette.sand2))
@@ -187,16 +193,19 @@ struct TimelineEntryDetailView: View {
}
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(i.value)
.font(.system(size: 30, weight: .bold, design: .rounded))
.font(.tjScaled( 30, weight: .bold, design: .rounded))
.foregroundStyle(i.status == .normal ? Tj.Palette.text : Tj.Palette.brick)
if !i.unit.isEmpty {
Text(i.unit).font(.system(size: 14)).foregroundStyle(Tj.Palette.text3)
Text(i.unit).font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text3)
}
}
divider
if !i.range.isEmpty { field(String(appLoc: "参考范围"), i.range) }
field(String(appLoc: "记录时间"), Self.dateTimeText(i.capturedAt))
field(String(appLoc: "来源"), i.report?.title ?? i.source.label)
if let report = i.report {
evidenceButton(for: i, assets: report.assets)
}
if let note = i.note, !note.isEmpty { field(String(appLoc: "备注"), note) }
}
}
@@ -215,9 +224,9 @@ struct TimelineEntryDetailView: View {
}
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("\(sys.value)/\(dia?.value ?? "")")
.font(.system(size: 30, weight: .bold, design: .rounded))
.font(.tjScaled( 30, weight: .bold, design: .rounded))
.foregroundStyle(combined == .normal ? Tj.Palette.text : Tj.Palette.brick)
Text("mmHg").font(.system(size: 14)).foregroundStyle(Tj.Palette.text3)
Text("mmHg").font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text3)
}
divider
if !sys.range.isEmpty { field(String(appLoc: "参考范围"), sys.range) }
@@ -237,10 +246,10 @@ struct TimelineEntryDetailView: View {
HStack(spacing: 8) {
TjBadge(text: r.type.label, style: .neutral)
Text(Self.dateText(r.reportDate))
.font(.system(size: 12)).foregroundStyle(Tj.Palette.text3)
.font(.tjScaled( 12)).foregroundStyle(Tj.Palette.text3)
if !r.assets.isEmpty {
Text(String(appLoc: "原图\(r.assets.count)"))
.font(.system(size: 12)).foregroundStyle(Tj.Palette.text3)
.font(.tjScaled( 12)).foregroundStyle(Tj.Palette.text3)
}
}
if let inst = r.institution, !inst.isEmpty {
@@ -251,8 +260,8 @@ struct TimelineEntryDetailView: View {
if let sum = r.summary, !sum.isEmpty {
card {
Text(String(appLoc: "摘要"))
.font(.system(size: 12, weight: .semibold)).foregroundStyle(Tj.Palette.text2)
Text(sum).font(.system(size: 14)).foregroundStyle(Tj.Palette.text)
.font(.tjScaled( 12, weight: .semibold)).foregroundStyle(Tj.Palette.text2)
Text(sum).font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text)
.fixedSize(horizontal: false, vertical: true)
}
}
@@ -260,15 +269,18 @@ struct TimelineEntryDetailView: View {
if !r.indicators.isEmpty {
card {
Text(String(appLoc: "指标"))
.font(.system(size: 12, weight: .semibold)).foregroundStyle(Tj.Palette.text2)
.font(.tjScaled( 12, weight: .semibold)).foregroundStyle(Tj.Palette.text2)
ForEach(sorted) { ind in
HStack {
Text(ind.name).font(.system(size: 14)).foregroundStyle(Tj.Palette.text)
Spacer(minLength: 8)
Text(ind.unit.isEmpty ? ind.value : "\(ind.value) \(ind.unit)")
.font(.system(size: 13, design: .monospaced))
.foregroundStyle(ind.status == .normal ? Tj.Palette.text2 : Tj.Palette.brick)
statusChip(ind.status)
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(ind.name).font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text)
Spacer(minLength: 8)
Text(ind.unit.isEmpty ? ind.value : "\(ind.value) \(ind.unit)")
.font(.tjScaled( 13, design: .monospaced))
.foregroundStyle(ind.status == .normal ? Tj.Palette.text2 : Tj.Palette.brick)
statusChip(ind.status)
}
evidenceButton(for: ind, assets: r.assets)
}
}
}
@@ -286,9 +298,9 @@ struct TimelineEntryDetailView: View {
VStack(alignment: .leading, spacing: 16) {
card {
Text(Self.dateTimeText(d.createdAt))
.font(.system(size: 12)).foregroundStyle(Tj.Palette.text3)
.font(.tjScaled( 12)).foregroundStyle(Tj.Palette.text3)
Text(d.content)
.font(.system(size: 15))
.font(.tjScaled( 15))
.foregroundStyle(Tj.Palette.text)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -309,7 +321,7 @@ struct TimelineEntryDetailView: View {
Spacer()
if s.isOngoing {
Text(String(appLoc: "进行中"))
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.brick)
.padding(.horizontal, 8).padding(.vertical, 4)
.background(Capsule().fill(Tj.Palette.brick.opacity(0.14)))
@@ -346,16 +358,36 @@ struct TimelineEntryDetailView: View {
private func field(_ label: String, _ value: String) -> some View {
HStack(alignment: .top, spacing: 12) {
Text(label).font(.system(size: 13)).foregroundStyle(Tj.Palette.text3)
Text(label).font(.tjScaled( 13)).foregroundStyle(Tj.Palette.text3)
Spacer(minLength: 12)
Text(value)
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text)
.multilineTextAlignment(.trailing)
.fixedSize(horizontal: false, vertical: true)
}
}
@ViewBuilder
private func evidenceButton(for indicator: Indicator, assets: [Asset]) -> some View {
if indicator.hasEvidenceBox,
let page = indicator.sourcePageIndex,
assets.indices.contains(page) {
Button {
evidenceTarget = indicator
} label: {
Label(String(appLoc: "查看原图位置"), systemImage: "viewfinder")
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.ink)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Capsule().fill(Tj.Palette.leaf.opacity(0.14)))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
private var divider: some View {
Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1)
}
@@ -370,8 +402,8 @@ struct TimelineEntryDetailView: View {
case .normal: text = String(appLoc: "正常"); color = Tj.Palette.leaf; arrow = ""
}
return HStack(spacing: 3) {
if !arrow.isEmpty { Text(arrow).font(.system(size: 11, weight: .bold)) }
Text(text).font(.system(size: 12, weight: .semibold))
if !arrow.isEmpty { Text(arrow).font(.tjScaled( 11, weight: .bold)) }
Text(text).font(.tjScaled( 12, weight: .semibold))
}
.foregroundStyle(color)
.padding(.horizontal, 8)
@@ -387,3 +419,142 @@ struct TimelineEntryDetailView: View {
d.formatted(.dateTime.year().month().day())
}
}
private struct EvidenceImagePreview: View {
@Environment(\.dismiss) private var dismiss
let report: Report
let indicator: Indicator
@State private var selection: Int
init(report: Report, indicator: Indicator) {
self.report = report
self.indicator = indicator
let page = indicator.sourcePageIndex ?? 0
_selection = State(initialValue: min(max(page, 0), max(report.assets.count - 1, 0)))
}
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Button { dismiss() } label: {
Image(systemName: "xmark")
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.frame(width: 32, height: 32)
.background(Circle().fill(Tj.Palette.sand2))
}
VStack(alignment: .leading, spacing: 2) {
Text(indicator.name)
.font(.tjScaled( 16, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("\(selection + 1) 页 · 原图证据")
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
}
Spacer()
}
.padding(.horizontal, 20)
.padding(.vertical, 14)
.background(Tj.Palette.sand)
.overlay(alignment: .bottom) {
Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1)
}
TabView(selection: $selection) {
ForEach(Array(report.assets.enumerated()), id: \.offset) { index, asset in
EvidenceImagePage(
asset: asset,
highlight: index == indicator.sourcePageIndex ? indicator.evidenceRect : nil
)
.tag(index)
.padding(16)
}
}
.tabViewStyle(.page(indexDisplayMode: report.assets.count > 1 ? .automatic : .never))
}
.background(Tj.Palette.sand.ignoresSafeArea())
.presentationDetents([.large])
.presentationDragIndicator(.visible)
.presentationBackground(Tj.Palette.sand)
}
}
private struct EvidenceImagePage: View {
let asset: Asset
let highlight: CGRect?
private var image: UIImage? {
try? FileVault.shared.loadImage(relativePath: asset.relativePath)
}
var body: some View {
GeometryReader { geo in
if let image {
ZStack {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: geo.size.width, height: geo.size.height)
if let highlight {
EvidenceHighlightOverlay(imageSize: image.size, normalizedRect: highlight)
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Tj.Palette.paper)
.clipShape(RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
)
} else {
TjPlaceholder(label: String(appLoc: "原图无法读取"))
.frame(width: geo.size.width, height: geo.size.height)
}
}
}
}
private struct EvidenceHighlightOverlay: View {
let imageSize: CGSize
let normalizedRect: CGRect
var body: some View {
GeometryReader { geo in
let fitted = fittedRect(imageSize: imageSize, containerSize: geo.size)
let rect = CGRect(
x: fitted.minX + normalizedRect.minX * fitted.width,
y: fitted.minY + normalizedRect.minY * fitted.height,
width: normalizedRect.width * fitted.width,
height: normalizedRect.height * fitted.height
)
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(Tj.Palette.brick.opacity(0.16))
.overlay(
RoundedRectangle(cornerRadius: 4, style: .continuous)
.stroke(Tj.Palette.brick, lineWidth: 2)
)
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
.shadow(color: Tj.Palette.brick.opacity(0.24), radius: 8, y: 2)
}
.allowsHitTesting(false)
}
private func fittedRect(imageSize: CGSize, containerSize: CGSize) -> CGRect {
guard imageSize.width > 0,
imageSize.height > 0,
containerSize.width > 0,
containerSize.height > 0 else {
return .zero
}
let scale = min(containerSize.width / imageSize.width, containerSize.height / imageSize.height)
let size = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
return CGRect(
x: (containerSize.width - size.width) / 2,
y: (containerSize.height - size.height) / 2,
width: size.width,
height: size.height
)
}
}

View File

@@ -9,7 +9,7 @@ struct TimelineRow: View {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(entry.kind.accent.opacity(0.12))
Image(systemName: entry.kind.icon)
.font(.system(size: 14, weight: .semibold))
.font(.tjScaled( 14, weight: .semibold))
.foregroundStyle(entry.kind.accent)
}
.frame(width: 36, height: 36)
@@ -25,12 +25,12 @@ struct TimelineRow: View {
VStack(alignment: .leading, spacing: 2) {
Text("\(entry.date.timelineLabel) · \(entry.subtitle)")
.font(.system(size: 11))
.font(.tjScaled( 11))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text3)
.lineLimit(1)
Text(entry.title)
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
.truncationMode(.tail)
@@ -38,7 +38,7 @@ struct TimelineRow: View {
Spacer(minLength: 8)
if let trailing = entry.trailing {
Text(trailing)
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.font(.tjScaled( 12, weight: .semibold, design: .monospaced))
.foregroundStyle(entry.trailingIsAlert ? Tj.Palette.brick : Tj.Palette.text2)
.lineLimit(1)
.fixedSize()

View File

@@ -66,7 +66,7 @@ struct CalendarMonthGrid: View {
HStack(spacing: 4) {
ForEach(weekdayLabels, id: \.self) { w in
Text(w)
.font(.system(size: 11, weight: .medium))
.font(.tjScaled( 11, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
.frame(maxWidth: .infinity)
}
@@ -123,7 +123,7 @@ private struct DayCellView: View {
VStack(spacing: 2) {
Text("\(dayNumber)")
.font(.system(size: 13,
.font(.tjScaled( 13,
weight: (isToday || isSelected) ? .bold : .regular,
design: .default))
.foregroundStyle(textColor)
@@ -137,7 +137,7 @@ private struct DayCellView: View {
}
if ranges.count > 2 {
Text("+\(ranges.count - 2)")
.font(.system(size: 7, design: .monospaced))
.font(.tjScaled( 7, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}

View File

@@ -62,7 +62,7 @@ private struct MiniMonth: View {
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(monthLabel)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
LazyVGrid(columns: microColumns, spacing: 2) {

View File

@@ -104,7 +104,7 @@ struct DayDetailContent: View {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 4) {
Text(dateLine)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.tracking(0.5)
.foregroundStyle(Tj.Palette.text3)
Text(dayLabel)
@@ -114,7 +114,7 @@ struct DayDetailContent: View {
Spacer()
if totalCount > 0 {
Text("\(totalCount)")
.font(.system(size: 12, design: .monospaced))
.font(.tjScaled( 12, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -140,11 +140,11 @@ struct DayDetailContent: View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text(title)
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.tracking(0.3)
.foregroundStyle(Tj.Palette.text2)
Text("\(count)")
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
@@ -162,17 +162,17 @@ struct DayDetailContent: View {
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 6) {
Text(s.name)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text(state.badge)
.font(.system(size: 10, weight: .semibold))
.font(.tjScaled( 10, weight: .semibold))
.foregroundStyle(state.badgeFg)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Capsule().fill(state.badgeBg))
}
Text("\(state.subtitle) · 持续 \(formatDuration(s.duration))")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
Spacer(minLength: 6)
@@ -181,7 +181,7 @@ struct DayDetailContent: View {
endingSymptom = s
} label: {
Text("结束")
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.padding(.horizontal, 12)
.padding(.vertical, 6)
@@ -200,24 +200,24 @@ struct DayDetailContent: View {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(indicatorAccent(i).opacity(0.12))
Image(systemName: "drop.fill")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(indicatorAccent(i))
}
.frame(width: 32, height: 32)
VStack(alignment: .leading, spacing: 2) {
Text(i.name)
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
if !i.range.isEmpty {
Text("参考 \(i.range)")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
}
Spacer(minLength: 6)
Text("\(i.value) \(i.unit)\(arrow(i))")
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.tjScaled( 13, weight: .semibold, design: .monospaced))
.foregroundStyle(i.status == .normal ? Tj.Palette.text2 : Tj.Palette.brick)
.lineLimit(1)
.fixedSize()
@@ -235,23 +235,23 @@ struct DayDetailContent: View {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Tj.Palette.ink2.opacity(0.12))
Image(systemName: "doc.fill")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.ink2)
}
.frame(width: 32, height: 32)
VStack(alignment: .leading, spacing: 2) {
Text(r.title)
.font(.system(size: 14, weight: .medium))
.font(.tjScaled( 14, weight: .medium))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
Text("\(r.type.label) · 共 \(r.pageCount)")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
Spacer(minLength: 6)
if let summary {
Text(summary)
.font(.system(size: 11, weight: .semibold, design: .monospaced))
.font(.tjScaled( 11, weight: .semibold, design: .monospaced))
.foregroundStyle(Tj.Palette.brick)
}
}
@@ -263,7 +263,7 @@ struct DayDetailContent: View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(d.createdAt.formatted(date: .omitted, time: .shortened))
.font(.system(size: 11, design: .monospaced))
.font(.tjScaled( 11, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
@@ -284,7 +284,7 @@ struct DayDetailContent: View {
.frame(height: 90)
.frame(maxWidth: 240)
Text("点底部 + 号可以补一条")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
.padding(.vertical, 12)

View File

@@ -66,10 +66,10 @@ struct SeriesChartCard: View {
private var header: some View {
HStack(alignment: .lastTextBaseline, spacing: 10) {
Text(bucket.title)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
Text("\(allPoints.count) 条 · 近 \(daysSpanLabel)")
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
Spacer()
latestValueBadge
@@ -87,10 +87,10 @@ struct SeriesChartCard: View {
}
return HStack(spacing: 4) {
Text(joined)
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.font(.tjScaled( 14, weight: .semibold, design: .monospaced))
.foregroundStyle(anyAbnormal ? Tj.Palette.brick : Tj.Palette.text)
Text(bucket.unit)
.font(.system(size: 10, design: .monospaced))
.font(.tjScaled( 10, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
}
@@ -142,7 +142,7 @@ struct SeriesChartCard: View {
AxisGridLine().foregroundStyle(Tj.Palette.lineSoft)
AxisValueLabel()
.foregroundStyle(Tj.Palette.text3)
.font(.system(size: 10, design: .monospaced))
.font(.tjScaled( 10, design: .monospaced))
}
}
.chartYScale(domain: valueDomain ?? 0...1)
@@ -156,7 +156,7 @@ struct SeriesChartCard: View {
.fill(line.color)
.frame(width: 8, height: 8)
Text(line.label ?? line.seriesKey)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text2)
}
}

View File

@@ -99,7 +99,7 @@ struct TrendDetailView: View {
withAnimation(.snappy(duration: 0.2)) { range = r }
} label: {
Text(r.label)
.font(.system(size: 12, weight: range == r ? .semibold : .regular))
.font(.tjScaled( 12, weight: range == r ? .semibold : .regular))
.foregroundStyle(range == r ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity)
.padding(.vertical, 7)
@@ -210,7 +210,7 @@ struct TrendDetailView: View {
AxisGridLine().foregroundStyle(Tj.Palette.lineSoft)
AxisValueLabel()
.foregroundStyle(Tj.Palette.text3)
.font(.system(size: 10, design: .monospaced))
.font(.tjScaled( 10, design: .monospaced))
}
}
.chartYScale(domain: valueDomain ?? 0...1)
@@ -222,7 +222,7 @@ struct TrendDetailView: View {
HStack(spacing: 5) {
Circle().fill(line.color).frame(width: 8, height: 8)
Text(line.label ?? line.seriesKey)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text2)
}
}
@@ -265,20 +265,20 @@ struct TrendDetailView: View {
VStack(alignment: .leading, spacing: 10) {
if filteredLines.count > 1, let label = line.label {
Text(label)
.font(.system(size: 12, weight: .semibold))
.font(.tjScaled( 12, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
}
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(latest.map { fmt($0.value) } ?? "")
.font(.system(size: 28, weight: .bold, design: .monospaced))
.font(.tjScaled( 28, weight: .bold, design: .monospaced))
.foregroundStyle((latest?.status ?? .normal) == .normal ? Tj.Palette.text : Tj.Palette.brick)
Text(bucket.unit)
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
if let delta = deltaText(latest: latest, prev: prev) {
Text(delta.text)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.tjScaled( 13, weight: .semibold, design: .monospaced))
.foregroundStyle(delta.color)
}
}
@@ -294,10 +294,10 @@ struct TrendDetailView: View {
private func statCell(_ label: String, _ value: String) -> some View {
VStack(spacing: 3) {
Text(value)
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.font(.tjScaled( 14, weight: .semibold, design: .monospaced))
.foregroundStyle(Tj.Palette.text)
Text(label)
.font(.system(size: 10))
.font(.tjScaled( 10))
.foregroundStyle(Tj.Palette.text3)
}
.frame(maxWidth: .infinity)
@@ -323,10 +323,10 @@ struct TrendDetailView: View {
private var aiPlaceholder: some View {
HStack(spacing: 8) {
Image(systemName: "sparkles")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Text("AI 趋势解读即将上线")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
@@ -364,7 +364,7 @@ struct TrendDetailView: View {
private var pointsList: some View {
VStack(alignment: .leading, spacing: 10) {
Text("全部记录")
.font(.system(size: 13, weight: .semibold))
.font(.tjScaled( 13, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
VStack(spacing: 8) {
ForEach(pointRows) { row in
@@ -382,7 +382,7 @@ struct TrendDetailView: View {
private func pointRowView(_ row: PointRow) -> some View {
HStack(spacing: 12) {
Text(row.day.formatted(.dateTime.year().month(.abbreviated).day()))
.font(.system(size: 13))
.font(.tjScaled( 13))
.foregroundStyle(Tj.Palette.text2)
Spacer(minLength: 8)
HStack(spacing: 10) {
@@ -393,14 +393,14 @@ struct TrendDetailView: View {
Circle().fill(line.color).frame(width: 6, height: 6)
}
Text(fmt(p.value) + arrow(p.status))
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.font(.tjScaled( 13, weight: .semibold, design: .monospaced))
.foregroundStyle(p.status == .normal ? Tj.Palette.text : Tj.Palette.brick)
}
}
}
}
Image(systemName: "chevron.right")
.font(.system(size: 11, weight: .medium))
.font(.tjScaled( 11, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(12)

View File

@@ -19,11 +19,11 @@ struct TrendRow: View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 3) {
Text(bucket.title)
.font(.system(size: 15, weight: .semibold))
.font(.tjScaled( 15, weight: .semibold))
.foregroundStyle(Tj.Palette.text)
.lineLimit(1)
Text(subtitle)
.font(.system(size: 11))
.font(.tjScaled( 11))
.foregroundStyle(Tj.Palette.text3)
}
@@ -34,17 +34,17 @@ struct TrendRow: View {
VStack(alignment: .trailing, spacing: 2) {
Text(latestValue)
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.font(.tjScaled( 14, weight: .semibold, design: .monospaced))
.foregroundStyle(anyLatestAbnormal ? Tj.Palette.brick : Tj.Palette.text)
.lineLimit(1)
Text(bucket.unit)
.font(.system(size: 9, design: .monospaced))
.font(.tjScaled( 9, design: .monospaced))
.foregroundStyle(Tj.Palette.text3)
}
.fixedSize()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.font(.tjScaled( 12, weight: .medium))
.foregroundStyle(Tj.Palette.text3)
}
.padding(14)

View File

@@ -63,7 +63,7 @@ struct TrendsView: View {
.font(.tjH2())
.foregroundStyle(Tj.Palette.text)
Text("\(buckets.count)")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
Spacer()
}
@@ -87,7 +87,7 @@ struct TrendsView: View {
.frame(height: 120)
.frame(maxWidth: 260)
Text("同一指标记录满 2 次后,会在这里出现时间序列")
.font(.system(size: 12))
.font(.tjScaled( 12))
.foregroundStyle(Tj.Palette.text3)
.multilineTextAlignment(.center)
}