feat: 添加拍药盒功能和语音直达入口 - 实现拍药盒扫描流程,支持本地OCR识别药品信息 - 在日记页面添加拍药盒和记症状的三选一入口 - 优化按钮点击区域,确保符合苹果HIG最小命中区标准 - 添加用药记录到时间线的独立分类显示 - 实现长按+号语音直达功能,支持语音意图分类跳转 - 更新项目配置文件,启用代码分析和死代码剥离选项 - 增加多项本地化字符串支持新功能 ```
75 lines
2.5 KiB
Swift
75 lines
2.5 KiB
Swift
import SwiftUI
|
|
import PhotosUI
|
|
|
|
/// VisionKit 在模拟器不可用,demo / 验证场景走 PhotosPicker 回退选已有照片。
|
|
/// 真机正式录入走 DocumentScannerView。
|
|
struct PhotoPickerSheet: View {
|
|
let onFinish: ([UIImage]) -> Void
|
|
let onCancel: () -> Void
|
|
|
|
@State private var selection: [PhotosPickerItem] = []
|
|
@State private var loading = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
Image(systemName: "photo.on.rectangle.angled")
|
|
.font(.tjScaled( 56))
|
|
.foregroundStyle(Tj.Palette.text3)
|
|
Text("模拟器没有摄像头,从相册选一张化验单/体检报告")
|
|
.font(.tjScaled( 13))
|
|
.foregroundStyle(Tj.Palette.text2)
|
|
.multilineTextAlignment(.center)
|
|
|
|
PhotosPicker(selection: $selection,
|
|
maxSelectionCount: 5,
|
|
matching: .images) {
|
|
Text("从相册选 ≤5 张")
|
|
.font(.tjScaled( 14, weight: .semibold))
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 12)
|
|
.background(Tj.Palette.ink)
|
|
.foregroundStyle(Tj.Palette.paper)
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
Button(action: onCancel) {
|
|
Text("取消")
|
|
.foregroundStyle(Tj.Palette.text3)
|
|
.padding(.horizontal, 24)
|
|
.frame(minHeight: 44) // HIG 最小命中区
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
if loading {
|
|
ProgressView().tint(Tj.Palette.ink)
|
|
}
|
|
}
|
|
.padding(28)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Tj.Palette.sand.ignoresSafeArea())
|
|
.onChange(of: selection) { _, newValue in
|
|
guard !newValue.isEmpty else { return }
|
|
loadImages(from: newValue)
|
|
}
|
|
}
|
|
|
|
private func loadImages(from items: [PhotosPickerItem]) {
|
|
loading = true
|
|
Task {
|
|
var images: [UIImage] = []
|
|
for item in items {
|
|
if let data = try? await item.loadTransferable(type: Data.self),
|
|
let img = UIImage(data: data) {
|
|
images.append(img)
|
|
}
|
|
}
|
|
await MainActor.run {
|
|
loading = false
|
|
if images.isEmpty { onCancel() }
|
|
else { onFinish(images) }
|
|
}
|
|
}
|
|
}
|
|
}
|