```
refactor: 重命名项目名称从"体己"到"康康" 将整个项目的目录结构从"体己"重命名为"康康",包括所有源代码文件、 资源文件、测试文件以及Xcode项目配置文件。此更改涉及项目中所有的 文件路径和应用入口点(App/TijiApp.swift → App/KangkangApp.swift)。 ```
This commit is contained in:
159
康康/Features/Quick/A1ViewfinderView.swift
Normal file
159
康康/Features/Quick/A1ViewfinderView.swift
Normal file
@@ -0,0 +1,159 @@
|
||||
import SwiftUI
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
struct A1ViewfinderView: View {
|
||||
var onShoot: () -> Void
|
||||
var onClose: () -> Void
|
||||
|
||||
@State private var dotPulse = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Color(red: 0.04, green: 0.047, blue: 0.04).ignoresSafeArea()
|
||||
|
||||
mockCameraPreview(screenHeight: geometry.size.height)
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: onClose) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(Color.white)
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.top, 50)
|
||||
|
||||
topHint
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
SmartFramer()
|
||||
.allowsHitTesting(false)
|
||||
.ignoresSafeArea()
|
||||
|
||||
identifiedPill
|
||||
.padding(.top, geometry.size.height * 0.62 - 20)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
bottomControls
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(iOS)
|
||||
.statusBarHidden(false)
|
||||
#endif
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
private func mockCameraPreview(screenHeight: CGFloat) -> some View {
|
||||
RadialGradient(
|
||||
colors: [Color.white.opacity(0.05), Color.clear],
|
||||
center: .init(x: 0.5, y: 0.3),
|
||||
startRadius: 20,
|
||||
endRadius: 400
|
||||
)
|
||||
.overlay(alignment: .center) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("总胆固醇 TC 5.42 mmol/L").opacity(0.65)
|
||||
Text("甘油三酯 TG 1.78 mmol/L").opacity(0.65)
|
||||
Text("低密度脂蛋白 3.84 mmol/L ↑").fontWeight(.semibold).opacity(1)
|
||||
Text("高密度脂蛋白 1.21 mmol/L").opacity(0.65)
|
||||
Text("载脂蛋白 A1 1.42 g/L").opacity(0.45)
|
||||
Text("载脂蛋白 B 1.04 g/L").opacity(0.45)
|
||||
}
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.padding(.vertical, 20)
|
||||
.padding(.horizontal, 18)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(red: 0.96, green: 0.93, blue: 0.87).opacity(0.92))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
|
||||
.rotationEffect(.degrees(-1.2))
|
||||
.shadow(color: .black.opacity(0.45), radius: 15, x: 0, y: 12)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, screenHeight * 0.20)
|
||||
}
|
||||
}
|
||||
|
||||
private var topHint: some View {
|
||||
Text("对准异常的那一行就好 · 不用拍整张")
|
||||
.font(.system(size: 12))
|
||||
.tracking(0.5)
|
||||
.foregroundStyle(Color.white.opacity(0.92))
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 7)
|
||||
.background(Capsule().fill(Color(red: 0.08, green: 0.11, blue: 0.094).opacity(0.7)))
|
||||
.padding(.top, 6)
|
||||
}
|
||||
|
||||
private var identifiedPill: some View {
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
.fill(Tj.Palette.paper)
|
||||
.frame(width: 6, height: 6)
|
||||
.opacity(dotPulse ? 1 : 0.35)
|
||||
Text("AI 已识别到 1 项指标")
|
||||
.font(.system(size: 11))
|
||||
.tracking(0.5)
|
||||
}
|
||||
.foregroundStyle(Tj.Palette.paper)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(Capsule().fill(Color(red: 0.37, green: 0.47, blue: 0.31).opacity(0.85)))
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 2.2).repeatForever(autoreverses: true)) {
|
||||
dotPulse.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomControls: some View {
|
||||
HStack {
|
||||
CircleIconButton(icon: "bolt.fill", size: 44) { }
|
||||
Spacer()
|
||||
Button(action: onShoot) {
|
||||
ZStack {
|
||||
Circle().fill(Tj.Palette.ink)
|
||||
Circle().strokeBorder(Tj.Palette.paper, lineWidth: 4)
|
||||
}
|
||||
.frame(width: 72, height: 72)
|
||||
.overlay(
|
||||
Circle().strokeBorder(Color.white.opacity(0.2), lineWidth: 1)
|
||||
.frame(width: 76, height: 76)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
Spacer()
|
||||
CircleIconButton(icon: "photo.on.rectangle", size: 44) { }
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
|
||||
private struct CircleIconButton: View {
|
||||
let icon: String
|
||||
let size: CGFloat
|
||||
let action: () -> Void
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
ZStack {
|
||||
Circle().fill(Color.white.opacity(0.12))
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundStyle(Tj.Palette.paper)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
180
康康/Features/Quick/A2ConfirmView.swift
Normal file
180
康康/Features/Quick/A2ConfirmView.swift
Normal file
@@ -0,0 +1,180 @@
|
||||
import SwiftUI
|
||||
|
||||
struct A2ConfirmView: View {
|
||||
var onSave: () -> Void
|
||||
var onNext: () -> Void
|
||||
var onBack: () -> Void
|
||||
|
||||
@State private var expanded = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
croppedPhoto.padding(.bottom, 14)
|
||||
resultCard.padding(.bottom, 16)
|
||||
actions
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.bottom, 18)
|
||||
}
|
||||
}
|
||||
.background(Tj.Palette.sand.ignoresSafeArea())
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(spacing: 6) {
|
||||
Button(action: onBack) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
Text("核对识别结果")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
Spacer()
|
||||
Text("识别用时 0.4s · 本地")
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Capsule().fill(Tj.Palette.sand2))
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 4)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
private var croppedPhoto: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Text("低密度脂蛋白 3.84 mmol/L ↑")
|
||||
.font(.system(size: 13, design: .monospaced))
|
||||
.fontWeight(.semibold)
|
||||
.tracking(0.3)
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.padding(.vertical, 14)
|
||||
.padding(.horizontal, 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(red: 0.96, green: 0.93, blue: 0.87).opacity(0.92))
|
||||
.clipShape(RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous))
|
||||
.shadow(color: Color(red: 0.196, green: 0.157, blue: 0.098).opacity(0.06),
|
||||
radius: 2, x: 0, y: 1)
|
||||
Text("已裁剪")
|
||||
.font(.system(size: 9))
|
||||
.tracking(0.5)
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
.padding(.top, 8)
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
}
|
||||
|
||||
private var resultCard: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("指标名 · 可编辑")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
Text("低密度脂蛋白胆固醇")
|
||||
.font(.system(size: 19, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
Text("LDL-C")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
Spacer()
|
||||
TjBadge(text: "偏高", style: .brick)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
FieldBox(label: "数值") {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text("3.84")
|
||||
.font(.system(size: 30, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.brick)
|
||||
Text("mmol/L")
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
}
|
||||
FieldBox(label: "参考范围") {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text("< 3.40")
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.foregroundStyle(Tj.Palette.text2)
|
||||
Text("mmol/L")
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button { withAnimation { expanded.toggle() } } label: {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
RoundedRectangle(cornerRadius: 2, style: .continuous)
|
||||
.fill(Tj.Palette.brick)
|
||||
.frame(width: 4)
|
||||
Text(expanded
|
||||
? "超过参考上限 0.44,属轻度偏高。建议关注饮食结构(减少动物脂肪摄入),3 个月内复查。若家族有心血管病史,可与医生沟通是否需要药物干预。"
|
||||
: "超过参考上限 0.44,属轻度偏高。点击展开详细解读 ›")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(Tj.Palette.text2)
|
||||
.lineSpacing(5)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||
.fill(Tj.Palette.sand)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(18)
|
||||
.tjCard()
|
||||
}
|
||||
|
||||
private var actions: some View {
|
||||
VStack(spacing: 10) {
|
||||
Button(action: onSave) {
|
||||
Text("保存到记录")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(TjPrimaryButton())
|
||||
|
||||
Button(action: onNext) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "camera.fill").font(.system(size: 14))
|
||||
Text("继续拍下一项")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(TjGhostButton())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct FieldBox<Content: View>: View {
|
||||
let label: String
|
||||
@ViewBuilder var content: Content
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(label)
|
||||
.font(.system(size: 10))
|
||||
.tracking(0.5)
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
content
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Tj.Radius.sm, style: .continuous)
|
||||
.strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
124
康康/Features/Quick/A3BatchView.swift
Normal file
124
康康/Features/Quick/A3BatchView.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
import SwiftUI
|
||||
|
||||
struct A3BatchItem {
|
||||
let name: String
|
||||
let value: String
|
||||
let unit: String
|
||||
let range: String
|
||||
let status: IndicatorStatus
|
||||
}
|
||||
|
||||
struct A3BatchView: View {
|
||||
var onAddMore: () -> Void
|
||||
var onFinish: () -> Void
|
||||
var onBack: () -> Void
|
||||
|
||||
let items: [A3BatchItem] = [
|
||||
.init(name: "低密度脂蛋白胆固醇", value: "3.84", unit: "mmol/L", range: "< 3.40", status: .high),
|
||||
.init(name: "甘油三酯 TG", value: "1.78", unit: "mmol/L", range: "< 1.70", status: .high),
|
||||
.init(name: "空腹血糖 GLU", value: "5.4", unit: "mmol/L", range: "3.9–6.1", status: .normal),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 10) {
|
||||
ForEach(Array(items.enumerated()), id: \.offset) { idx, it in
|
||||
BatchRow(index: idx + 1, item: it)
|
||||
}
|
||||
addRow
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
HStack(spacing: 10) {
|
||||
Button {
|
||||
onFinish()
|
||||
} label: {
|
||||
Text("全部保存(\(items.count))").frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(TjPrimaryButton())
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 14)
|
||||
}
|
||||
.background(Tj.Palette.sand.ignoresSafeArea())
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(spacing: 6) {
|
||||
Button(action: onBack) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("本次已记录 \(items.count) 项")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
Text("核对后一次保存")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
Spacer()
|
||||
Text("· · ·")
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 4)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
|
||||
private var addRow: some View {
|
||||
Button(action: onAddMore) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "camera").font(.system(size: 14))
|
||||
Text("再拍一项")
|
||||
.font(.system(size: 13))
|
||||
}
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
|
||||
.strokeBorder(Tj.Palette.line, style: StrokeStyle(lineWidth: 1.5, dash: [4, 4]))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
private struct BatchRow: View {
|
||||
let index: Int
|
||||
let item: A3BatchItem
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
TjPlaceholder(label: "#\(index)")
|
||||
.frame(width: 60, height: 44)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.name)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(Tj.Palette.text)
|
||||
.lineLimit(1)
|
||||
Text("范围 \(item.range) \(item.unit)")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(Tj.Palette.text3)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(item.value)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundStyle(item.status == .high ? Tj.Palette.brick : Tj.Palette.text)
|
||||
TjBadge(text: item.status == .high ? "偏高" : "正常",
|
||||
style: item.status == .high ? .brick : .leaf)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.tjCard()
|
||||
}
|
||||
}
|
||||
60
康康/Features/Quick/QuickCaptureFlow.swift
Normal file
60
康康/Features/Quick/QuickCaptureFlow.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
import SwiftUI
|
||||
|
||||
private enum QuickStep: Hashable {
|
||||
case viewfinder
|
||||
case confirm
|
||||
case batch
|
||||
}
|
||||
|
||||
struct QuickCaptureFlow: View {
|
||||
var onClose: () -> Void
|
||||
|
||||
@State private var step: QuickStep = .viewfinder
|
||||
@State private var snapCount = 0
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch step {
|
||||
case .viewfinder:
|
||||
A1ViewfinderView(
|
||||
onShoot: {
|
||||
snapCount += 1
|
||||
withAnimation(.easeInOut(duration: 0.25)) { step = .confirm }
|
||||
},
|
||||
onClose: onClose
|
||||
)
|
||||
.transition(.opacity)
|
||||
|
||||
case .confirm:
|
||||
A2ConfirmView(
|
||||
onSave: {
|
||||
if snapCount >= 2 {
|
||||
withAnimation { step = .batch }
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
onNext: {
|
||||
withAnimation { step = .viewfinder }
|
||||
},
|
||||
onBack: {
|
||||
withAnimation { step = .viewfinder }
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
|
||||
case .batch:
|
||||
A3BatchView(
|
||||
onAddMore: {
|
||||
withAnimation { step = .viewfinder }
|
||||
},
|
||||
onFinish: onClose,
|
||||
onBack: {
|
||||
withAnimation { step = .confirm }
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
康康/Features/Quick/SmartFramer.swift
Normal file
100
康康/Features/Quick/SmartFramer.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SmartFramer: View {
|
||||
var radius: CGFloat = 10
|
||||
var height: CGFloat = 56
|
||||
@State private var breathing = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ZStack {
|
||||
Color.black.opacity(0.32)
|
||||
.mask(
|
||||
Rectangle()
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: radius, style: .continuous)
|
||||
.frame(height: height)
|
||||
.padding(.horizontal, geo.size.width * 0.08)
|
||||
.blendMode(.destinationOut)
|
||||
)
|
||||
.compositingGroup()
|
||||
)
|
||||
|
||||
RoundedRectangle(cornerRadius: radius + 4, style: .continuous)
|
||||
.stroke(Color(red: 0.95, green: 0.78, blue: 0.45), lineWidth: 1.5)
|
||||
.shadow(color: Color(red: 0.95, green: 0.78, blue: 0.45).opacity(0.5), radius: 8)
|
||||
.frame(height: height + 8)
|
||||
.padding(.horizontal, geo.size.width * 0.08 - 4)
|
||||
.opacity(breathing ? 1 : 0.35)
|
||||
|
||||
cornerMarks(in: geo.size)
|
||||
}
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 2.2).repeatForever(autoreverses: true)) {
|
||||
breathing.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cornerMarks(in size: CGSize) -> some View {
|
||||
let inset = size.width * 0.08
|
||||
return ZStack {
|
||||
ForEach(Corner.allCases, id: \.self) { corner in
|
||||
CornerMark(corner: corner, radius: radius)
|
||||
.frame(width: 18, height: 18)
|
||||
.position(corner.position(in: size, inset: inset, frameHeight: height))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Corner: CaseIterable {
|
||||
case tl, tr, bl, br
|
||||
func position(in size: CGSize, inset: CGFloat, frameHeight: CGFloat) -> CGPoint {
|
||||
let centerY = size.height / 2
|
||||
let top = centerY - frameHeight / 2
|
||||
let bottom = centerY + frameHeight / 2
|
||||
switch self {
|
||||
case .tl: return CGPoint(x: inset, y: top)
|
||||
case .tr: return CGPoint(x: size.width - inset, y: top)
|
||||
case .bl: return CGPoint(x: inset, y: bottom)
|
||||
case .br: return CGPoint(x: size.width - inset, y: bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CornerMark: View {
|
||||
let corner: Corner
|
||||
let radius: CGFloat
|
||||
|
||||
var body: some View {
|
||||
Path { p in
|
||||
let r = min(radius, 8)
|
||||
switch corner {
|
||||
case .tl:
|
||||
p.move(to: CGPoint(x: 0, y: 18))
|
||||
p.addLine(to: CGPoint(x: 0, y: r))
|
||||
p.addQuadCurve(to: CGPoint(x: r, y: 0), control: CGPoint(x: 0, y: 0))
|
||||
p.addLine(to: CGPoint(x: 18, y: 0))
|
||||
case .tr:
|
||||
p.move(to: CGPoint(x: 0, y: 0))
|
||||
p.addLine(to: CGPoint(x: 18 - r, y: 0))
|
||||
p.addQuadCurve(to: CGPoint(x: 18, y: r), control: CGPoint(x: 18, y: 0))
|
||||
p.addLine(to: CGPoint(x: 18, y: 18))
|
||||
case .bl:
|
||||
p.move(to: CGPoint(x: 0, y: 0))
|
||||
p.addLine(to: CGPoint(x: 0, y: 18 - r))
|
||||
p.addQuadCurve(to: CGPoint(x: r, y: 18), control: CGPoint(x: 0, y: 18))
|
||||
p.addLine(to: CGPoint(x: 18, y: 18))
|
||||
case .br:
|
||||
p.move(to: CGPoint(x: 0, y: 18))
|
||||
p.addLine(to: CGPoint(x: 18 - r, y: 18))
|
||||
p.addQuadCurve(to: CGPoint(x: 18, y: 18 - r), control: CGPoint(x: 18, y: 18))
|
||||
p.addLine(to: CGPoint(x: 18, y: 0))
|
||||
}
|
||||
}
|
||||
.stroke(Tj.Palette.paper, style: StrokeStyle(lineWidth: 2.5, lineCap: .round))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user