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