From a75aeed767106faaa6cf72b47be8b40a26748e3d Mon Sep 17 00:00:00 2001 From: empty Date: Sat, 7 Feb 2026 21:12:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9B=BF=E6=8D=A2=E7=A1=AC?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=80=BC=E4=B8=BA=20DesignTokens=20+=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A7=A6=E8=A7=89=E5=8F=8D=E9=A6=88=E5=92=8C?= =?UTF-8?q?=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 硬编码值替换: - spacing: 20/24/16/12/8/4 → DesignTokens.Spacing.xl/xxl/lg/md/sm/xs - cornerRadius: 16/8/4 → DesignTokens.Radius.lg/sm/xs - padding(.top, 8) / padding(.leading, 4) → DesignTokens.Spacing.sm/xs 生成按钮替换: - 手动实现的渐变按钮替换为 SoftPrimaryButton 组件 - 添加底部安全间距 padding(.bottom, DesignTokens.Spacing.sm) 触觉反馈(iOS 17+ sensoryFeedback): - 比例选择切换:.selection - 生成按钮点击:.impact(weight: .medium) - 裁剪手势结束:.impact(weight: .light) 动画优化: - AspectRatioButton 添加选中状态过渡动画 - CropOverlay 添加比例切换平滑过渡动画 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../to-live-photo/Views/EditorView.swift | 110 +++++++++--------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/to-live-photo/to-live-photo/Views/EditorView.swift b/to-live-photo/to-live-photo/Views/EditorView.swift index 6722fdf..1eee7c9 100644 --- a/to-live-photo/to-live-photo/Views/EditorView.swift +++ b/to-live-photo/to-live-photo/Views/EditorView.swift @@ -46,6 +46,9 @@ struct EditorView: View { // 视频诊断 @State private var videoDiagnosis: VideoDiagnosis? + // 触觉反馈触发 + @State private var generateTapCount: Int = 0 + /// 是否使用 iPad 分栏布局(regular 宽度 + 横屏) private var useIPadLayout: Bool { horizontalSizeClass == .regular @@ -67,13 +70,16 @@ struct EditorView: View { .onDisappear { player?.pause() } + .sensoryFeedback(.selection, trigger: selectedAspectRatio) + .sensoryFeedback(.impact(weight: .medium), trigger: generateTapCount) + .sensoryFeedback(.impact(weight: .light), trigger: lastCropScale) } // MARK: - iPhone 布局(单列滚动) @ViewBuilder private var iPhoneLayout: some View { ScrollView { - VStack(spacing: 20) { + VStack(spacing: DesignTokens.Spacing.xl) { cropPreview(height: 360) if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty { @@ -96,9 +102,9 @@ struct EditorView: View { // MARK: - iPad 布局(左右分栏) @ViewBuilder private var iPadLayout: some View { - HStack(alignment: .top, spacing: 24) { + HStack(alignment: .top, spacing: DesignTokens.Spacing.xxl) { // 左侧:视频预览 - VStack(spacing: 16) { + VStack(spacing: DesignTokens.Spacing.lg) { cropPreview(height: 500, dynamicHeight: true) if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty { @@ -111,7 +117,7 @@ struct EditorView: View { // 右侧:参数控制 ScrollView { - VStack(spacing: 16) { + VStack(spacing: DesignTokens.Spacing.lg) { coverFrameSection durationSection keyFrameSection @@ -178,8 +184,8 @@ struct EditorView: View { } } .frame(width: containerWidth, height: containerHeight) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .background(Color.black.clipShape(RoundedRectangle(cornerRadius: 16))) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg)) + .background(Color.black.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg))) } .frame(height: height) } @@ -187,7 +193,7 @@ struct EditorView: View { // MARK: - 比例模板选择 @ViewBuilder private var aspectRatioSection: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) { HStack { Image(systemName: "aspectratio") .foregroundStyle(.tint) @@ -195,7 +201,7 @@ struct EditorView: View { .font(.headline) } - HStack(spacing: 8) { + HStack(spacing: DesignTokens.Spacing.sm) { ForEach(AspectRatioTemplate.allCases, id: \.self) { template in AspectRatioButton( template: template, @@ -221,7 +227,7 @@ struct EditorView: View { // MARK: - 封面帧预览 @ViewBuilder private var coverFrameSection: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) { HStack { Image(systemName: "photo") .foregroundStyle(.tint) @@ -234,15 +240,15 @@ struct EditorView: View { } } - HStack(spacing: 12) { + HStack(spacing: DesignTokens.Spacing.md) { if let coverImage { Image(uiImage: coverImage) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 80, height: 120) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)) } else { - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.sm) .fill(Color.softPressed) .frame(width: 80, height: 120) .overlay { @@ -251,7 +257,7 @@ struct EditorView: View { } } - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.xs) { Text(String(localized: "editor.coverFrameHint1")) .font(.caption) .foregroundColor(.textSecondary) @@ -269,7 +275,7 @@ struct EditorView: View { // MARK: - 时长控制 @ViewBuilder private var durationSection: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) { HStack { Image(systemName: "timer") .foregroundStyle(.tint) @@ -306,7 +312,7 @@ struct EditorView: View { // MARK: - 封面帧时间选择 @ViewBuilder private var keyFrameSection: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) { HStack { Image(systemName: "clock") .foregroundStyle(.tint) @@ -344,7 +350,7 @@ struct EditorView: View { // MARK: - AI 超分辨率开关 @ViewBuilder private var aiEnhanceSection: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) { Toggle(isOn: $aiEnhanceEnabled) { HStack { Image(systemName: "wand.and.stars.inverse") @@ -368,8 +374,8 @@ struct EditorView: View { // 模型下载进度 if aiModelDownloading { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.sm) { + HStack(spacing: DesignTokens.Spacing.sm) { ProgressView() .scaleEffect(0.8) Text(String(localized: "editor.aiModelDownloading")) @@ -384,13 +390,13 @@ struct EditorView: View { .font(.caption2) .foregroundColor(.textSecondary) } - .padding(.leading, 4) + .padding(.leading, DesignTokens.Spacing.xs) } if aiEnhanceEnabled && !aiModelDownloading { VStack(alignment: .leading, spacing: 6) { if aiModelNeedsDownload { - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "arrow.down.circle") .foregroundStyle(.orange) .font(.caption) @@ -398,21 +404,21 @@ struct EditorView: View { .font(.caption) } } - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "sparkles") .foregroundStyle(Color.accentPurple) .font(.caption) Text(String(localized: "editor.aiResolutionBoost")) .font(.caption) } - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "clock") .foregroundStyle(Color.accentPurple) .font(.caption) Text(String(localized: "editor.aiProcessingTime")) .font(.caption) } - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "cpu") .foregroundStyle(Color.accentPurple) .font(.caption) @@ -421,11 +427,11 @@ struct EditorView: View { } } .foregroundColor(.textSecondary) - .padding(.leading, 4) + .padding(.leading, DesignTokens.Spacing.xs) } if !AIEnhancer.isAvailable() { - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "exclamationmark.triangle") .foregroundStyle(.yellow) .font(.caption) @@ -433,7 +439,7 @@ struct EditorView: View { .font(.caption) .foregroundColor(.textSecondary) } - .padding(.top, 4) + .padding(.top, DesignTokens.Spacing.xs) } } .padding(DesignTokens.Spacing.lg) @@ -448,7 +454,7 @@ struct EditorView: View { // MARK: - 兼容模式开关 @ViewBuilder private var compatibilitySection: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) { Toggle(isOn: $compatibilityMode) { HStack { Image(systemName: "gearshape.2") @@ -466,28 +472,28 @@ struct EditorView: View { if compatibilityMode { VStack(alignment: .leading, spacing: 6) { - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .font(.caption) Text(String(localized: "editor.resolution720p")) .font(.caption) } - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .font(.caption) Text(String(localized: "editor.framerate30fps")) .font(.caption) } - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .font(.caption) Text(String(localized: "editor.codecH264")) .font(.caption) } - HStack(spacing: 4) { + HStack(spacing: DesignTokens.Spacing.xs) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .font(.caption) @@ -496,7 +502,7 @@ struct EditorView: View { } } .foregroundColor(.textSecondary) - .padding(.leading, 4) + .padding(.leading, DesignTokens.Spacing.xs) } } .padding(DesignTokens.Spacing.lg) @@ -507,7 +513,7 @@ struct EditorView: View { // MARK: - 诊断建议 @ViewBuilder private func diagnosisSection(diagnosis: VideoDiagnosis) -> some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.yellow) @@ -517,12 +523,12 @@ struct EditorView: View { ForEach(diagnosis.suggestions.indices, id: \.self) { index in let suggestion = diagnosis.suggestions[index] - HStack(alignment: .top, spacing: 12) { + HStack(alignment: .top, spacing: DesignTokens.Spacing.md) { Image(systemName: suggestion.icon) .foregroundStyle(suggestion.iconColor) .frame(width: 24) - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: DesignTokens.Spacing.xs) { Text(suggestion.title) .font(.subheadline) .fontWeight(.medium) @@ -555,22 +561,15 @@ struct EditorView: View { // MARK: - 生成按钮 @ViewBuilder private var generateButton: some View { - Button { + SoftPrimaryButton( + String(localized: "editor.generateButton"), + icon: "wand.and.stars", + gradient: Color.gradientPrimary + ) { startProcessing() - } label: { - HStack { - Image(systemName: "wand.and.stars") - Text(String(localized: "editor.generateButton")) - } - .font(.headline) - .frame(maxWidth: .infinity) - .padding() - .background(Color.gradientPrimary) - .foregroundColor(.white) - .clipShape(RoundedRectangle(cornerRadius: 14)) } - .buttonStyle(ScaleButtonStyle()) - .padding(.top, 8) + .padding(.top, DesignTokens.Spacing.sm) + .padding(.bottom, DesignTokens.Spacing.sm) } // MARK: - 方法 @@ -758,6 +757,7 @@ struct EditorView: View { } private func startProcessing() { + generateTapCount += 1 Analytics.shared.log(.editorGenerateClick, parameters: [ "trimStart": trimStart, "trimEnd": trimEnd, @@ -795,15 +795,15 @@ struct AspectRatioButton: View { var body: some View { Button(action: action) { - VStack(spacing: 4) { + VStack(spacing: DesignTokens.Spacing.xs) { // 比例图标 - RoundedRectangle(cornerRadius: 4) + RoundedRectangle(cornerRadius: DesignTokens.Spacing.xs) .stroke(isSelected ? Color.accentPurple : Color.textSecondary, lineWidth: 2) .frame(width: iconWidth, height: iconHeight) .background( isSelected ? Color.accentPurple.opacity(0.1) : Color.clear ) - .clipShape(RoundedRectangle(cornerRadius: 4)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Spacing.xs)) Text(template.displayName) .font(.caption2) @@ -814,6 +814,7 @@ struct AspectRatioButton: View { .padding(.vertical, DesignTokens.Spacing.sm) .background(isSelected ? Color.accentPurple.opacity(0.1) : Color.clear) .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)) + .animation(DesignTokens.Animation.quick, value: isSelected) } .buttonStyle(.plain) .accessibilityElement(children: .ignore) @@ -858,17 +859,18 @@ struct CropOverlay: View { .mask( Rectangle() .overlay( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.sm) .frame(width: cropSize.width, height: cropSize.height) .blendMode(.destinationOut) ) ) // 裁剪框边框 - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: DesignTokens.Radius.sm) .stroke(Color.white, lineWidth: 2) .frame(width: cropSize.width, height: cropSize.height) } + .animation(DesignTokens.Animation.standard, value: aspectRatio) } .allowsHitTesting(false) }