refactor: 替换硬编码值为 DesignTokens + 添加触觉反馈和动画

硬编码值替换:
- 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) <noreply@anthropic.com>
This commit is contained in:
empty
2026-02-07 21:12:37 +08:00
parent 1556dfd167
commit a75aeed767

View File

@@ -46,6 +46,9 @@ struct EditorView: View {
// //
@State private var videoDiagnosis: VideoDiagnosis? @State private var videoDiagnosis: VideoDiagnosis?
//
@State private var generateTapCount: Int = 0
/// 使 iPad regular + /// 使 iPad regular +
private var useIPadLayout: Bool { private var useIPadLayout: Bool {
horizontalSizeClass == .regular horizontalSizeClass == .regular
@@ -67,13 +70,16 @@ struct EditorView: View {
.onDisappear { .onDisappear {
player?.pause() player?.pause()
} }
.sensoryFeedback(.selection, trigger: selectedAspectRatio)
.sensoryFeedback(.impact(weight: .medium), trigger: generateTapCount)
.sensoryFeedback(.impact(weight: .light), trigger: lastCropScale)
} }
// MARK: - iPhone // MARK: - iPhone
@ViewBuilder @ViewBuilder
private var iPhoneLayout: some View { private var iPhoneLayout: some View {
ScrollView { ScrollView {
VStack(spacing: 20) { VStack(spacing: DesignTokens.Spacing.xl) {
cropPreview(height: 360) cropPreview(height: 360)
if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty { if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty {
@@ -96,9 +102,9 @@ struct EditorView: View {
// MARK: - iPad // MARK: - iPad
@ViewBuilder @ViewBuilder
private var iPadLayout: some View { 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) cropPreview(height: 500, dynamicHeight: true)
if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty { if let diagnosis = videoDiagnosis, !diagnosis.suggestions.isEmpty {
@@ -111,7 +117,7 @@ struct EditorView: View {
// //
ScrollView { ScrollView {
VStack(spacing: 16) { VStack(spacing: DesignTokens.Spacing.lg) {
coverFrameSection coverFrameSection
durationSection durationSection
keyFrameSection keyFrameSection
@@ -178,8 +184,8 @@ struct EditorView: View {
} }
} }
.frame(width: containerWidth, height: containerHeight) .frame(width: containerWidth, height: containerHeight)
.clipShape(RoundedRectangle(cornerRadius: 16)) .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg))
.background(Color.black.clipShape(RoundedRectangle(cornerRadius: 16))) .background(Color.black.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg)))
} }
.frame(height: height) .frame(height: height)
} }
@@ -187,7 +193,7 @@ struct EditorView: View {
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private var aspectRatioSection: some View { private var aspectRatioSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
HStack { HStack {
Image(systemName: "aspectratio") Image(systemName: "aspectratio")
.foregroundStyle(.tint) .foregroundStyle(.tint)
@@ -195,7 +201,7 @@ struct EditorView: View {
.font(.headline) .font(.headline)
} }
HStack(spacing: 8) { HStack(spacing: DesignTokens.Spacing.sm) {
ForEach(AspectRatioTemplate.allCases, id: \.self) { template in ForEach(AspectRatioTemplate.allCases, id: \.self) { template in
AspectRatioButton( AspectRatioButton(
template: template, template: template,
@@ -221,7 +227,7 @@ struct EditorView: View {
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private var coverFrameSection: some View { private var coverFrameSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
HStack { HStack {
Image(systemName: "photo") Image(systemName: "photo")
.foregroundStyle(.tint) .foregroundStyle(.tint)
@@ -234,15 +240,15 @@ struct EditorView: View {
} }
} }
HStack(spacing: 12) { HStack(spacing: DesignTokens.Spacing.md) {
if let coverImage { if let coverImage {
Image(uiImage: coverImage) Image(uiImage: coverImage)
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: 80, height: 120) .frame(width: 80, height: 120)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm))
} else { } else {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)
.fill(Color.softPressed) .fill(Color.softPressed)
.frame(width: 80, height: 120) .frame(width: 80, height: 120)
.overlay { .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")) Text(String(localized: "editor.coverFrameHint1"))
.font(.caption) .font(.caption)
.foregroundColor(.textSecondary) .foregroundColor(.textSecondary)
@@ -269,7 +275,7 @@ struct EditorView: View {
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private var durationSection: some View { private var durationSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
HStack { HStack {
Image(systemName: "timer") Image(systemName: "timer")
.foregroundStyle(.tint) .foregroundStyle(.tint)
@@ -306,7 +312,7 @@ struct EditorView: View {
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private var keyFrameSection: some View { private var keyFrameSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
HStack { HStack {
Image(systemName: "clock") Image(systemName: "clock")
.foregroundStyle(.tint) .foregroundStyle(.tint)
@@ -344,7 +350,7 @@ struct EditorView: View {
// MARK: - AI // MARK: - AI
@ViewBuilder @ViewBuilder
private var aiEnhanceSection: some View { private var aiEnhanceSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
Toggle(isOn: $aiEnhanceEnabled) { Toggle(isOn: $aiEnhanceEnabled) {
HStack { HStack {
Image(systemName: "wand.and.stars.inverse") Image(systemName: "wand.and.stars.inverse")
@@ -368,8 +374,8 @@ struct EditorView: View {
// //
if aiModelDownloading { if aiModelDownloading {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.sm) {
HStack(spacing: 8) { HStack(spacing: DesignTokens.Spacing.sm) {
ProgressView() ProgressView()
.scaleEffect(0.8) .scaleEffect(0.8)
Text(String(localized: "editor.aiModelDownloading")) Text(String(localized: "editor.aiModelDownloading"))
@@ -384,13 +390,13 @@ struct EditorView: View {
.font(.caption2) .font(.caption2)
.foregroundColor(.textSecondary) .foregroundColor(.textSecondary)
} }
.padding(.leading, 4) .padding(.leading, DesignTokens.Spacing.xs)
} }
if aiEnhanceEnabled && !aiModelDownloading { if aiEnhanceEnabled && !aiModelDownloading {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
if aiModelNeedsDownload { if aiModelNeedsDownload {
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "arrow.down.circle") Image(systemName: "arrow.down.circle")
.foregroundStyle(.orange) .foregroundStyle(.orange)
.font(.caption) .font(.caption)
@@ -398,21 +404,21 @@ struct EditorView: View {
.font(.caption) .font(.caption)
} }
} }
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "sparkles") Image(systemName: "sparkles")
.foregroundStyle(Color.accentPurple) .foregroundStyle(Color.accentPurple)
.font(.caption) .font(.caption)
Text(String(localized: "editor.aiResolutionBoost")) Text(String(localized: "editor.aiResolutionBoost"))
.font(.caption) .font(.caption)
} }
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "clock") Image(systemName: "clock")
.foregroundStyle(Color.accentPurple) .foregroundStyle(Color.accentPurple)
.font(.caption) .font(.caption)
Text(String(localized: "editor.aiProcessingTime")) Text(String(localized: "editor.aiProcessingTime"))
.font(.caption) .font(.caption)
} }
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "cpu") Image(systemName: "cpu")
.foregroundStyle(Color.accentPurple) .foregroundStyle(Color.accentPurple)
.font(.caption) .font(.caption)
@@ -421,11 +427,11 @@ struct EditorView: View {
} }
} }
.foregroundColor(.textSecondary) .foregroundColor(.textSecondary)
.padding(.leading, 4) .padding(.leading, DesignTokens.Spacing.xs)
} }
if !AIEnhancer.isAvailable() { if !AIEnhancer.isAvailable() {
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "exclamationmark.triangle") Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
.font(.caption) .font(.caption)
@@ -433,7 +439,7 @@ struct EditorView: View {
.font(.caption) .font(.caption)
.foregroundColor(.textSecondary) .foregroundColor(.textSecondary)
} }
.padding(.top, 4) .padding(.top, DesignTokens.Spacing.xs)
} }
} }
.padding(DesignTokens.Spacing.lg) .padding(DesignTokens.Spacing.lg)
@@ -448,7 +454,7 @@ struct EditorView: View {
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private var compatibilitySection: some View { private var compatibilitySection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
Toggle(isOn: $compatibilityMode) { Toggle(isOn: $compatibilityMode) {
HStack { HStack {
Image(systemName: "gearshape.2") Image(systemName: "gearshape.2")
@@ -466,28 +472,28 @@ struct EditorView: View {
if compatibilityMode { if compatibilityMode {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(.green)
.font(.caption) .font(.caption)
Text(String(localized: "editor.resolution720p")) Text(String(localized: "editor.resolution720p"))
.font(.caption) .font(.caption)
} }
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(.green)
.font(.caption) .font(.caption)
Text(String(localized: "editor.framerate30fps")) Text(String(localized: "editor.framerate30fps"))
.font(.caption) .font(.caption)
} }
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(.green)
.font(.caption) .font(.caption)
Text(String(localized: "editor.codecH264")) Text(String(localized: "editor.codecH264"))
.font(.caption) .font(.caption)
} }
HStack(spacing: 4) { HStack(spacing: DesignTokens.Spacing.xs) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(.green)
.font(.caption) .font(.caption)
@@ -496,7 +502,7 @@ struct EditorView: View {
} }
} }
.foregroundColor(.textSecondary) .foregroundColor(.textSecondary)
.padding(.leading, 4) .padding(.leading, DesignTokens.Spacing.xs)
} }
} }
.padding(DesignTokens.Spacing.lg) .padding(DesignTokens.Spacing.lg)
@@ -507,7 +513,7 @@ struct EditorView: View {
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private func diagnosisSection(diagnosis: VideoDiagnosis) -> some View { private func diagnosisSection(diagnosis: VideoDiagnosis) -> some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.md) {
HStack { HStack {
Image(systemName: "exclamationmark.triangle.fill") Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.yellow) .foregroundStyle(.yellow)
@@ -517,12 +523,12 @@ struct EditorView: View {
ForEach(diagnosis.suggestions.indices, id: \.self) { index in ForEach(diagnosis.suggestions.indices, id: \.self) { index in
let suggestion = diagnosis.suggestions[index] let suggestion = diagnosis.suggestions[index]
HStack(alignment: .top, spacing: 12) { HStack(alignment: .top, spacing: DesignTokens.Spacing.md) {
Image(systemName: suggestion.icon) Image(systemName: suggestion.icon)
.foregroundStyle(suggestion.iconColor) .foregroundStyle(suggestion.iconColor)
.frame(width: 24) .frame(width: 24)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: DesignTokens.Spacing.xs) {
Text(suggestion.title) Text(suggestion.title)
.font(.subheadline) .font(.subheadline)
.fontWeight(.medium) .fontWeight(.medium)
@@ -555,22 +561,15 @@ struct EditorView: View {
// MARK: - // MARK: -
@ViewBuilder @ViewBuilder
private var generateButton: some View { private var generateButton: some View {
Button { SoftPrimaryButton(
String(localized: "editor.generateButton"),
icon: "wand.and.stars",
gradient: Color.gradientPrimary
) {
startProcessing() 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, DesignTokens.Spacing.sm)
.padding(.top, 8) .padding(.bottom, DesignTokens.Spacing.sm)
} }
// MARK: - // MARK: -
@@ -758,6 +757,7 @@ struct EditorView: View {
} }
private func startProcessing() { private func startProcessing() {
generateTapCount += 1
Analytics.shared.log(.editorGenerateClick, parameters: [ Analytics.shared.log(.editorGenerateClick, parameters: [
"trimStart": trimStart, "trimStart": trimStart,
"trimEnd": trimEnd, "trimEnd": trimEnd,
@@ -795,15 +795,15 @@ struct AspectRatioButton: View {
var body: some View { var body: some View {
Button(action: action) { 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) .stroke(isSelected ? Color.accentPurple : Color.textSecondary, lineWidth: 2)
.frame(width: iconWidth, height: iconHeight) .frame(width: iconWidth, height: iconHeight)
.background( .background(
isSelected ? Color.accentPurple.opacity(0.1) : Color.clear isSelected ? Color.accentPurple.opacity(0.1) : Color.clear
) )
.clipShape(RoundedRectangle(cornerRadius: 4)) .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Spacing.xs))
Text(template.displayName) Text(template.displayName)
.font(.caption2) .font(.caption2)
@@ -814,6 +814,7 @@ struct AspectRatioButton: View {
.padding(.vertical, DesignTokens.Spacing.sm) .padding(.vertical, DesignTokens.Spacing.sm)
.background(isSelected ? Color.accentPurple.opacity(0.1) : Color.clear) .background(isSelected ? Color.accentPurple.opacity(0.1) : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)) .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm))
.animation(DesignTokens.Animation.quick, value: isSelected)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
@@ -858,17 +859,18 @@ struct CropOverlay: View {
.mask( .mask(
Rectangle() Rectangle()
.overlay( .overlay(
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)
.frame(width: cropSize.width, height: cropSize.height) .frame(width: cropSize.width, height: cropSize.height)
.blendMode(.destinationOut) .blendMode(.destinationOut)
) )
) )
// //
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)
.stroke(Color.white, lineWidth: 2) .stroke(Color.white, lineWidth: 2)
.frame(width: cropSize.width, height: cropSize.height) .frame(width: cropSize.width, height: cropSize.height)
} }
.animation(DesignTokens.Animation.standard, value: aspectRatio)
} }
.allowsHitTesting(false) .allowsHitTesting(false)
} }