缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。
当您提供代码差异后,我将按照以下格式生成: ``` <type>(<scope>): <subject> <body> ``` 其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user