```
feat(AI): 优化AIRuntime任务取消机制并增强安全保护 - 在AI推理流中添加Task.checkCancellation()检查,使消费者取消时能快速退出 - 为异步流添加onTermination回调以取消内部Task,与LLMSession一致 - 实现SwiftData store的completeUnlessOpen文件保护,提升数据安全性 - 在store备份过程中同样应用加密保护 feat(home): 优化主页交互体验并统一详情查看功能 - 在主页"最近记录"中点击任意条目可打开只读详情sheet - 将时间线详情解析逻辑统一收敛到TimelineDetail.resolve方法 - 修复血压条目的精确反查逻辑,避免时间窗匹配错误 feat(archive): 新增提醒任务汇总卡并完善档案库功能 - 在档案库页面新增提醒任务汇总卡,显示总数和启用状态 - 添加按更新时间倒序合并的提醒标题预览功能 - 实现RemindersListView导航路由,统一管理提醒任务 - 优化导出列表显示,优先使用中文标签展示 feat(me): 优化个人中心界面并改进语言设置体验 - 将个人中心标题改为内容文字渲染,解决导航栏背景问题 - 为语言选择器添加个性化图标,使用本族语代表字区分 - 修复语言设置视图的图标显示逻辑 feat(timeline): 新增记录详情页删除功能并优化图表显示 - 在时间线详情页添加永久删除按钮和确认弹窗 - 实现完整的删除逻辑,包括SwiftData硬删和Vault原图unlink - 修复系列图表的数值范围计算,处理同值数据的对称留白 - 优化血压图表合并逻辑,只保留有数据点的线条 refactor(calendar): 修复DST切换导致的月份天数计算错误 - 使用calendar.range(of:.day,in:.month)替代日期间隔计算 - 避免在夏令时切换月份出现天数偏差问题 fix(ui): 修复多个UI组件的交互响应区域问题 - 为纯描边按钮和胶囊添加contentShape以扩大点击区域 - 修复提醒行展开按钮尺寸,保证不同提醒类型的垂直对齐 ```
This commit is contained in:
@@ -10,19 +10,56 @@ enum TimelineDetail {
|
||||
case report(Report)
|
||||
case diary(DiaryEntry)
|
||||
case symptom(Symptom)
|
||||
|
||||
/// 把时间线条目反查回源记录(id 形如 `<kind>-<persistentModelID>` / `bp-<sysID>-<diaID>`)。
|
||||
/// 主页「最近记录」与档案库 C1 共用同一套反查,避免逻辑重复。无法定位源记录时返回 nil。
|
||||
static func resolve(for entry: TimelineEntry,
|
||||
indicators: [Indicator],
|
||||
reports: [Report],
|
||||
diaries: [DiaryEntry],
|
||||
symptoms: [Symptom]) -> TimelineDetail? {
|
||||
switch entry.kind {
|
||||
case .report:
|
||||
return reports.first { "report-\($0.persistentModelID)" == entry.id }
|
||||
.map(TimelineDetail.report)
|
||||
case .diary:
|
||||
return diaries.first { "diary-\($0.persistentModelID)" == entry.id }
|
||||
.map(TimelineDetail.diary)
|
||||
case .symptom:
|
||||
return symptoms.first { "symptom-\($0.persistentModelID)" == entry.id }
|
||||
.map(TimelineDetail.symptom)
|
||||
case .indicator:
|
||||
if let i = indicators.first(where: { "indicator-\($0.persistentModelID)" == entry.id }) {
|
||||
return .indicator(i)
|
||||
}
|
||||
// 合并血压条目:bp-<sysID>-<diaID>
|
||||
if entry.id.hasPrefix("bp-"),
|
||||
let sys = indicators.first(where: { entry.id.hasPrefix("bp-\($0.persistentModelID)-") }) {
|
||||
// 用 id 里编码的 diaID 精确反查,不再用 ±5s 时间窗近似匹配
|
||||
//(后者在同日多次量血压时会把详情配到错误的舒张读数)。
|
||||
let dia = indicators.first { entry.id.hasSuffix("-\($0.persistentModelID)") }
|
||||
return .bloodPressure(sys: sys, dia: dia)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 时间线条目的只读详情:展示该记录的完整字段。各类型一屏看完,不可编辑。
|
||||
struct TimelineEntryDetailView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var ctx
|
||||
let detail: TimelineDetail
|
||||
|
||||
@State private var showDeleteConfirm = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
bodyContent
|
||||
deleteButton
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
@@ -34,6 +71,63 @@ struct TimelineEntryDetailView: View {
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackground(Tj.Palette.sand)
|
||||
.presentationCornerRadius(Tj.Radius.xl)
|
||||
.alert(String(appLoc: "永久删除这条记录?"), isPresented: $showDeleteConfirm) {
|
||||
Button(String(appLoc: "删除"), role: .destructive) { performDelete() }
|
||||
Button(String(appLoc: "取消"), role: .cancel) { }
|
||||
} message: {
|
||||
Text("删除后无法恢复。")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 删除(永久:SwiftData 硬删 + Vault 原图 unlink,见 CLAUDE.md §6)
|
||||
|
||||
private var deleteButton: some View {
|
||||
Button(role: .destructive) { showDeleteConfirm = true } label: {
|
||||
Label(String(appLoc: "永久删除"), systemImage: "trash")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(Tj.Palette.brick.opacity(0.8))
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
|
||||
.strokeBorder(Tj.Palette.brick.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
// 纯描边按钮:补 contentShape 让整框可点(否则中间透明区点不到)。
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
private func performDelete() {
|
||||
switch detail {
|
||||
case .indicator(let i):
|
||||
deleteIndicator(i)
|
||||
case .bloodPressure(let sys, let dia):
|
||||
deleteIndicator(sys)
|
||||
if let dia { deleteIndicator(dia) }
|
||||
case .report(let r):
|
||||
// cascade 只删 Asset/Indicator 记录,Vault 里的 JPEG 要手动 unlink。
|
||||
var paths = Set(r.assets.map(\.relativePath))
|
||||
paths.formUnion(r.indicators.compactMap { $0.asset?.relativePath })
|
||||
for p in paths { try? FileVault.shared.remove(relativePath: p) }
|
||||
ctx.delete(r)
|
||||
case .diary(let d):
|
||||
ctx.delete(d)
|
||||
case .symptom(let s):
|
||||
ctx.delete(s)
|
||||
}
|
||||
try? ctx.save()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
/// 删一条指标:先 unlink 其原图文件 + Asset 记录(关系默认 nullify,不会自动级联),再删指标本身。
|
||||
private func deleteIndicator(_ i: Indicator) {
|
||||
if let asset = i.asset {
|
||||
try? FileVault.shared.remove(relativePath: asset.relativePath)
|
||||
ctx.delete(asset)
|
||||
}
|
||||
ctx.delete(i)
|
||||
}
|
||||
|
||||
// MARK: - Header
|
||||
|
||||
Reference in New Issue
Block a user