Files
legado-ios/App/Features/Settings/DiagnosticLogView.swift
T

99 lines
4.7 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
struct DiagnosticLogView: View {
@State private var entries: [DiagnosticLogEntry] = []
@State private var query = ""
@State private var level = "all"
@State private var confirmsClear = false
@State private var exportsLog = false
@State private var exportText = ""
@State private var errorMessage: String?
private var filtered: [DiagnosticLogEntry] {
entries.filter {
(level == "all" || $0.level.rawValue == level)
&& (query.isEmpty || "\($0.category) \($0.message)".localizedCaseInsensitiveContains(query))
}
}
var body: some View {
List {
Section {
Picker("级别", selection: $level) {
Text("全部").tag("all")
Text("信息").tag("info")
Text("警告").tag("warning")
Text("错误").tag("error")
}.pickerStyle(.segmented)
Text("\(entries.count) 条日志 · 下拉刷新")
.font(.footnote).foregroundStyle(.secondary)
} footer: {
Text("在“其他设置”开启记录后,会保存网络请求、JS 输出和运行错误。崩溃诊断由 iOS 延迟提供,可搜索“系统诊断”查看。")
}
if filtered.isEmpty {
ReadingUnavailableView(query.isEmpty ? "暂无日志" : "没有匹配的日志", systemImage: "doc.text.magnifyingglass")
} else {
ForEach(filtered) { entry in
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(entry.category).font(.caption.weight(.semibold))
Text(levelTitle(entry.level)).font(.caption)
.foregroundStyle(entry.level == .error ? .red : .secondary)
Spacer()
Text(entry.date, format: .dateTime.month().day().hour().minute().second())
.font(.caption2).foregroundStyle(.secondary)
}
Text(entry.message).font(.system(.footnote, design: .monospaced)).textSelection(.enabled)
}.padding(.vertical, 3)
}
}
}
.navigationTitle("日志与诊断")
.navigationBarTitleDisplayMode(.inline)
.searchable(text: $query, prompt: "搜索内容或分类")
.task { reload() }
.refreshable { reload() }
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu("日志操作", systemImage: "ellipsis") {
Button("刷新", systemImage: "arrow.clockwise") { reload() }
Button("导出全部日志", systemImage: "square.and.arrow.up") {
exportText = DiagnosticLog.shared.export()
exportsLog = true
}.disabled(entries.isEmpty)
Button("清空日志", systemImage: "trash", role: .destructive) { confirmsClear = true }
.disabled(entries.isEmpty)
}
}
}
.confirmationDialog("清空全部日志?", isPresented: $confirmsClear, titleVisibility: .visible) {
Button("清空日志", role: .destructive) {
do { try DiagnosticLog.shared.clear(); reload() }
catch { errorMessage = error.localizedDescription }
}
}
.fileExporter(isPresented: $exportsLog, document: DiagnosticLogDocument(text: exportText), contentType: .plainText, defaultFilename: "阅读诊断日志") { result in
if case .failure(let error) = result { errorMessage = error.localizedDescription }
}
.alert("日志操作失败", isPresented: Binding(get: { errorMessage != nil }, set: { if !$0 { errorMessage = nil } })) {
Button("好") { errorMessage = nil }
} message: { Text(errorMessage ?? "") }
}
private func reload() { entries = DiagnosticLog.shared.snapshot() }
private func levelTitle(_ level: DiagnosticLogEntry.Level) -> String {
switch level { case .info: "信息"; case .warning: "警告"; case .error: "错误" }
}
}
private struct DiagnosticLogDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
var text: String
init(text: String) { self.text = text }
init(configuration: ReadConfiguration) throws {
text = String(decoding: configuration.file.regularFileContents ?? Data(), as: UTF8.self)
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { .init(regularFileWithContents: Data(text.utf8)) }
}