- 替换 QuickCaptureFlow 和 ArchiveFlow 为 UnifiedCaptureFlow 统一流程 - 新增 VLSession 封装 Qwen2.5-VL 模型进行图像文本推理 - 实现 AIRuntime 中 VL 模型的准备和分析功能 - 添加 VLPrompts 定义体检化验单识别的 JSON 输出模板 - 创建 CaptureReviewForm 提供 VL 解析结果的可编辑表单界面 - 集成 VisionKit 文档扫描器支持真机多页文档扫描 - 为模拟器实现 PhotosPicker 回退方案选择已有照片 - 在 RootView 中统一使用 UnifiedCaptureFlow 处理快速和归档流程 - 添加 CustomMetricEditor 支持自定义监测指标的创建编辑删除 - 扩展 KangkangApp 模型配置以支持新数据类型 - 实现档案列表中症状结束功能通过时间线行点击触发
204 lines
6.3 KiB
Swift
204 lines
6.3 KiB
Swift
import SwiftUI
|
|
|
|
enum TjTab: String, Hashable, CaseIterable {
|
|
case home, records, trend, me
|
|
var label: String {
|
|
switch self {
|
|
case .home: return "主页"
|
|
case .records: return "记录"
|
|
case .trend: return "趋势"
|
|
case .me: return "我的"
|
|
}
|
|
}
|
|
var icon: String {
|
|
switch self {
|
|
case .home: return "house"
|
|
case .records: return "list.bullet.rectangle"
|
|
case .trend: return "chart.line.uptrend.xyaxis"
|
|
case .me: return "person.circle"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ActiveFlow: Identifiable {
|
|
case quick, archive
|
|
var id: String { String(describing: self) }
|
|
}
|
|
|
|
struct RootView: View {
|
|
@State private var tab: TjTab = .home
|
|
@State private var showRecordSheet = false
|
|
@State private var activeFlow: ActiveFlow?
|
|
@State private var showSymptomStart = false
|
|
@State private var showDiary = false
|
|
@State private var showIndicator = false
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
Group {
|
|
switch tab {
|
|
case .home: HomeView(onTapArchive: { tab = .records })
|
|
case .records: ArchiveListView()
|
|
case .trend: TrendsView()
|
|
case .me: MeView()
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
TabBar(active: tab,
|
|
onTap: { tab = $0 },
|
|
onTapRecord: { showRecordSheet = true })
|
|
}
|
|
.background(Tj.Palette.sand.ignoresSafeArea())
|
|
.sheet(isPresented: $showRecordSheet) {
|
|
RecordSheet { kind in
|
|
showRecordSheet = false
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
switch kind {
|
|
case .quick: activeFlow = .quick
|
|
case .archive: activeFlow = .archive
|
|
case .symptom: showSymptomStart = true
|
|
case .diary: showDiary = true
|
|
case .indicator: showIndicator = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showSymptomStart) {
|
|
SymptomStartSheet()
|
|
}
|
|
.sheet(isPresented: $showDiary) {
|
|
DiaryQuickSheet()
|
|
}
|
|
.sheet(isPresented: $showIndicator) {
|
|
IndicatorQuickSheet()
|
|
}
|
|
#if os(iOS)
|
|
.fullScreenCover(item: $activeFlow) { flow in
|
|
switch flow {
|
|
case .quick:
|
|
UnifiedCaptureFlow(onClose: { activeFlow = nil })
|
|
case .archive:
|
|
UnifiedCaptureFlow(onClose: { activeFlow = nil })
|
|
}
|
|
}
|
|
#else
|
|
.sheet(item: $activeFlow) { flow in
|
|
switch flow {
|
|
case .quick:
|
|
UnifiedCaptureFlow(onClose: { activeFlow = nil })
|
|
case .archive:
|
|
UnifiedCaptureFlow(onClose: { activeFlow = nil })
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private struct TabBar: View {
|
|
let active: TjTab
|
|
let onTap: (TjTab) -> Void
|
|
let onTapRecord: () -> Void
|
|
|
|
private let cornerRadius: CGFloat = 22
|
|
private let slotHeight: CGFloat = 34
|
|
|
|
var body: some View {
|
|
HStack(alignment: .bottom, spacing: 0) {
|
|
tabItem(.home)
|
|
tabItem(.records)
|
|
recordSlot
|
|
tabItem(.trend)
|
|
tabItem(.me)
|
|
}
|
|
.padding(.horizontal, 4)
|
|
.padding(.top, 10)
|
|
.padding(.bottom, 6)
|
|
.background(barBackground)
|
|
}
|
|
|
|
private var barBackground: some View {
|
|
UnevenRoundedRectangle(
|
|
topLeadingRadius: cornerRadius,
|
|
bottomLeadingRadius: 0,
|
|
bottomTrailingRadius: 0,
|
|
topTrailingRadius: cornerRadius,
|
|
style: .continuous
|
|
)
|
|
.fill(Tj.Palette.paper)
|
|
.overlay(alignment: .top) {
|
|
Rectangle()
|
|
.fill(Tj.Palette.lineSoft)
|
|
.frame(height: 1)
|
|
}
|
|
.shadow(color: Tj.Palette.ink.opacity(0.05), radius: 10, x: 0, y: -2)
|
|
}
|
|
|
|
private func tabItem(_ t: TjTab) -> some View {
|
|
let isActive = active == t
|
|
return Button { onTap(t) } label: {
|
|
VStack(spacing: 4) {
|
|
ZStack {
|
|
if isActive {
|
|
Capsule()
|
|
.fill(Tj.Palette.sand2)
|
|
.frame(width: 44, height: slotHeight - 6)
|
|
}
|
|
Image(systemName: t.icon)
|
|
.font(.system(size: 18, weight: isActive ? .semibold : .regular))
|
|
}
|
|
.frame(width: 50, height: slotHeight)
|
|
|
|
Text(t.label)
|
|
.font(.system(size: 11, weight: isActive ? .semibold : .regular))
|
|
}
|
|
.foregroundStyle(isActive ? Tj.Palette.ink : Tj.Palette.text3)
|
|
.frame(maxWidth: .infinity)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(TabPressStyle())
|
|
}
|
|
|
|
private var recordSlot: some View {
|
|
Button(action: onTapRecord) {
|
|
VStack(spacing: 4) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Tj.Palette.ink)
|
|
.overlay(
|
|
Circle()
|
|
.strokeBorder(Tj.Palette.paper, lineWidth: 2)
|
|
)
|
|
.shadow(color: Tj.Palette.ink.opacity(0.18),
|
|
radius: 4, x: 0, y: 2)
|
|
|
|
Image(systemName: "plus")
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundStyle(Tj.Palette.paper)
|
|
}
|
|
.frame(width: slotHeight, height: slotHeight)
|
|
|
|
Text("新建")
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.foregroundStyle(Tj.Palette.ink)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(TabPressStyle())
|
|
}
|
|
}
|
|
|
|
private struct TabPressStyle: ButtonStyle {
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.scaleEffect(configuration.isPressed ? 0.92 : 1.0)
|
|
.animation(.spring(response: 0.25, dampingFraction: 0.7),
|
|
value: configuration.isPressed)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RootView()
|
|
}
|