From e041cacd7daaccd318dd8bfb250cc452fd7b37a6 Mon Sep 17 00:00:00 2001 From: empty Date: Sat, 3 Jan 2026 23:15:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20UI=20=E8=AE=BE=E8=AE=A1=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BC=98=E5=8C=96=20-=20=E6=97=A0=E9=9A=9C=E7=A2=8D?= =?UTF-8?q?=E3=80=81=E6=B7=B1=E8=89=B2=E6=A8=A1=E5=BC=8F=E3=80=81=E5=AF=B9?= =?UTF-8?q?=E6=AF=94=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DesignSystem: 深色模式阴影适配,textMuted 对比度修复 - DesignSystem: SoftIconButton/SoftSlider/SoftProgressRing 添加 accessibilityLabel - EditorView: AspectRatioButton 添加无障碍支持,清理硬编码颜色 - WallpaperGuideView: 清理硬编码颜色 (Color.secondary → Color.softElevated) - Localizable: 修复 home.worksCount 插值 key 格式 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../to-live-photo/DesignSystem.swift | 90 +++++++++++++++++-- .../to-live-photo/Localizable.xcstrings | 74 ++++++++++++++- .../to-live-photo/Views/EditorView.swift | 62 +++++++------ .../to-live-photo/Views/HomeView.swift | 10 ++- .../to-live-photo/Views/SettingsView.swift | 6 +- .../Views/WallpaperGuideView.swift | 4 +- 6 files changed, 199 insertions(+), 47 deletions(-) diff --git a/to-live-photo/to-live-photo/DesignSystem.swift b/to-live-photo/to-live-photo/DesignSystem.swift index bff4766..2635895 100644 --- a/to-live-photo/to-live-photo/DesignSystem.swift +++ b/to-live-photo/to-live-photo/DesignSystem.swift @@ -41,6 +41,18 @@ enum DesignTokens { static let xxxl: CGFloat = 32 static let display: CGFloat = 40 } + + // MARK: 动画 + enum Animation { + /// 按钮、卡片等交互元素的弹性动画 + static let spring = SwiftUI.Animation.spring(response: 0.3, dampingFraction: 0.6) + /// 快速状态切换 + static let quick = SwiftUI.Animation.easeInOut(duration: 0.15) + /// 标准过渡 + static let standard = SwiftUI.Animation.easeInOut(duration: 0.25) + /// 进度条等连续动画 + static let smooth = SwiftUI.Animation.easeInOut(duration: 0.5) + } } // MARK: - 颜色系统 @@ -59,7 +71,8 @@ extension Color { // MARK: 文字色 static let textPrimary = Color(light: Color(hex: "#2D2D3A"), dark: Color(hex: "#E4E4EB")) static let textSecondary = Color(light: Color(hex: "#6B6B7B"), dark: Color(hex: "#A0A0B2")) - static let textMuted = Color(light: Color(hex: "#9999A9"), dark: Color(hex: "#6B6B7B")) + // 调整 textMuted 以满足 WCAG AA 对比度标准 (4.5:1) + static let textMuted = Color(light: Color(hex: "#777788"), dark: Color(hex: "#8888A0")) // MARK: 强调色 static let accentPurple = Color(hex: "#6366F1") @@ -137,42 +150,71 @@ extension Color { // MARK: - Soft UI 阴影 struct SoftShadow: ViewModifier { let isPressed: Bool + @Environment(\.colorScheme) private var colorScheme func body(content: Content) -> some View { content .shadow( - color: Color.black.opacity(isPressed ? 0.15 : 0.08), + color: shadowDark, radius: isPressed ? 4 : 8, x: isPressed ? 2 : 4, y: isPressed ? 2 : 4 ) .shadow( - color: Color.white.opacity(isPressed ? 0.5 : 0.7), + color: shadowLight, radius: isPressed ? 4 : 8, x: isPressed ? -2 : -4, y: isPressed ? -2 : -4 ) } + + // 深色模式下使用更深的暗影 + private var shadowDark: Color { + colorScheme == .dark + ? Color.black.opacity(isPressed ? 0.4 : 0.3) + : Color.black.opacity(isPressed ? 0.15 : 0.08) + } + + // 深色模式下使用微弱的高光(模拟边缘光) + private var shadowLight: Color { + colorScheme == .dark + ? Color.white.opacity(isPressed ? 0.03 : 0.05) + : Color.white.opacity(isPressed ? 0.5 : 0.7) + } } struct SoftInnerShadow: ViewModifier { + @Environment(\.colorScheme) private var colorScheme + func body(content: Content) -> some View { content .overlay( RoundedRectangle(cornerRadius: DesignTokens.Radius.lg) - .stroke(Color.black.opacity(0.06), lineWidth: 1) + .stroke(innerShadowDark, lineWidth: 1) .blur(radius: 2) .offset(x: 1, y: 1) .mask(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg).fill(LinearGradient(colors: [.black, .clear], startPoint: .topLeading, endPoint: .bottomTrailing))) ) .overlay( RoundedRectangle(cornerRadius: DesignTokens.Radius.lg) - .stroke(Color.white.opacity(0.5), lineWidth: 1) + .stroke(innerShadowLight, lineWidth: 1) .blur(radius: 2) .offset(x: -1, y: -1) .mask(RoundedRectangle(cornerRadius: DesignTokens.Radius.lg).fill(LinearGradient(colors: [.clear, .black], startPoint: .topLeading, endPoint: .bottomTrailing))) ) } + + private var innerShadowDark: Color { + colorScheme == .dark + ? Color.black.opacity(0.3) + : Color.black.opacity(0.06) + } + + private var innerShadowLight: Color { + colorScheme == .dark + ? Color.white.opacity(0.08) + : Color.white.opacity(0.5) + } } extension View { @@ -315,11 +357,13 @@ struct SoftSecondaryButton: View { struct SoftIconButton: View { let icon: String let isActive: Bool + let accessibilityLabel: String? let action: () -> Void - init(_ icon: String, isActive: Bool = false, action: @escaping () -> Void) { + init(_ icon: String, isActive: Bool = false, accessibilityLabel: String? = nil, action: @escaping () -> Void) { self.icon = icon self.isActive = isActive + self.accessibilityLabel = accessibilityLabel self.action = action } @@ -347,6 +391,9 @@ struct SoftIconButton: View { ) } .buttonStyle(ScaleButtonStyle()) + .accessibilityLabel(accessibilityLabel ?? icon) + .accessibilityAddTraits(.isButton) + .accessibilityAddTraits(isActive ? .isSelected : []) } } @@ -356,17 +403,20 @@ struct SoftProgressRing: View { let size: CGFloat let lineWidth: CGFloat let gradient: LinearGradient + let accessibilityLabel: String? init( progress: Double, size: CGFloat = 120, lineWidth: CGFloat = 8, - gradient: LinearGradient = Color.gradientPrimary + gradient: LinearGradient = Color.gradientPrimary, + accessibilityLabel: String? = nil ) { self.progress = progress self.size = size self.lineWidth = lineWidth self.gradient = gradient + self.accessibilityLabel = accessibilityLabel } var body: some View { @@ -394,8 +444,11 @@ struct SoftProgressRing: View { .stroke(gradient, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) .frame(width: size - 16, height: size - 16) .rotationEffect(.degrees(-90)) - .animation(.easeInOut(duration: 0.5), value: progress) + .animation(DesignTokens.Animation.smooth, value: progress) } + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityLabel ?? String(localized: "进度")) + .accessibilityValue(Text("\(Int(progress * 100))%")) } } @@ -475,15 +528,21 @@ struct SoftSlider: View { @Binding var value: Double let range: ClosedRange let gradient: LinearGradient + let accessibilityLabel: String + let step: Double init( value: Binding, in range: ClosedRange, - gradient: LinearGradient = Color.gradientPrimary + step: Double = 0.1, + gradient: LinearGradient = Color.gradientPrimary, + accessibilityLabel: String = "" ) { self._value = value self.range = range + self.step = step self.gradient = gradient + self.accessibilityLabel = accessibilityLabel } var body: some View { @@ -521,6 +580,19 @@ struct SoftSlider: View { } } .frame(height: 28) + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilityLabel.isEmpty ? String(localized: "滑块") : accessibilityLabel) + .accessibilityValue(Text(String(format: "%.1f", value))) + .accessibilityAdjustableAction { direction in + switch direction { + case .increment: + value = min(range.upperBound, value + step) + case .decrement: + value = max(range.lowerBound, value - step) + @unknown default: + break + } + } } } diff --git a/to-live-photo/to-live-photo/Localizable.xcstrings b/to-live-photo/to-live-photo/Localizable.xcstrings index aa6f02d..2eb17f3 100644 --- a/to-live-photo/to-live-photo/Localizable.xcstrings +++ b/to-live-photo/to-live-photo/Localizable.xcstrings @@ -254,7 +254,7 @@ } } }, - "home.worksCount" : { + "home.worksCount %lld" : { "extractionState" : "manual", "localizations" : { "en" : { @@ -1874,6 +1874,78 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "确定要清空最近作品记录吗?这不会删除相册中的 Live Photo。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "確定要清除最近作品記錄嗎?這不會刪除相簿中的 Live Photo。" } } } + }, + "已选中" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Selected" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已选中" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已選中" } } + } + }, + "进度" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Progress" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "进度" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "進度" } } + } + }, + "滑块" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Slider" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滑块" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "滑桿" } } + } + }, + "accessibility.settings" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Settings" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "设置" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定" } } + } + }, + "accessibility.play" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Play" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "播放" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "播放" } } + } + }, + "accessibility.pause" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Pause" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "暂停" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "暫停" } } + } + }, + "accessibility.livePhoto" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Live Photo" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Live Photo 作品" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Live Photo 作品" } } + } + }, + "accessibility.aspectRatio" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Aspect ratio %@" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "画面比例 %@" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "畫面比例 %@" } } + } + }, + "accessibility.duration" : { + "extractionState" : "manual", + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", "value" : "Duration" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "时长" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "時長" } } + } } }, "version" : "1.0" diff --git a/to-live-photo/to-live-photo/Views/EditorView.swift b/to-live-photo/to-live-photo/Views/EditorView.swift index 36900e1..4e1d723 100644 --- a/to-live-photo/to-live-photo/Views/EditorView.swift +++ b/to-live-photo/to-live-photo/Views/EditorView.swift @@ -86,8 +86,8 @@ struct EditorView: View { compatibilitySection generateButton } - .padding(.horizontal, 20) - .padding(.vertical, 16) + .padding(.horizontal, DesignTokens.Spacing.xl) + .padding(.vertical, DesignTokens.Spacing.lg) } } @@ -117,11 +117,11 @@ struct EditorView: View { compatibilitySection generateButton } - .padding(.vertical, 16) + .padding(.vertical, DesignTokens.Spacing.lg) } .frame(maxWidth: 360) } - .padding(24) + .padding(DesignTokens.Spacing.xxl) } // MARK: - iPad 裁剪预览(更大尺寸) @@ -239,10 +239,10 @@ struct EditorView: View { Text("选择适合壁纸的比例,锁屏推荐使用「锁屏」或「全屏」") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } .padding(16) - .background(Color.secondary.opacity(0.1)) + .background(Color.softElevated) .clipShape(RoundedRectangle(cornerRadius: 12)) } @@ -271,26 +271,26 @@ struct EditorView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } else { RoundedRectangle(cornerRadius: 8) - .fill(Color.secondary.opacity(0.2)) + .fill(Color.softPressed) .frame(width: 80, height: 120) .overlay { Image(systemName: "photo") - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } } VStack(alignment: .leading, spacing: 4) { Text("此图片将作为 Live Photo 的静态封面") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) Text("拖动下方滑杆选择封面时刻") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } } } .padding(16) - .background(Color.secondary.opacity(0.1)) + .background(Color.softElevated) .clipShape(RoundedRectangle(cornerRadius: 12)) } @@ -317,10 +317,10 @@ struct EditorView: View { Text("Live Photo 壁纸推荐时长:1 ~ 1.5 秒") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } .padding(16) - .background(Color.secondary.opacity(0.1)) + .background(Color.softElevated) .clipShape(RoundedRectangle(cornerRadius: 12)) } @@ -348,10 +348,10 @@ struct EditorView: View { Text("选择视频中的某一帧作为 Live Photo 的封面") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } .padding(16) - .background(Color.secondary.opacity(0.1)) + .background(Color.softElevated) .clipShape(RoundedRectangle(cornerRadius: 12)) } @@ -368,7 +368,7 @@ struct EditorView: View { .font(.headline) Text("使用 AI 提升封面画质") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } } } @@ -388,7 +388,7 @@ struct EditorView: View { .scaleEffect(0.8) Text("正在下载 AI 模型...") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } ProgressView(value: aiModelDownloadProgress) @@ -396,7 +396,7 @@ struct EditorView: View { Text(String(format: "%.0f%%", aiModelDownloadProgress * 100)) .font(.caption2) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } .padding(.leading, 4) } @@ -434,7 +434,7 @@ struct EditorView: View { .font(.caption) } } - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) .padding(.leading, 4) } @@ -445,7 +445,7 @@ struct EditorView: View { .font(.caption) Text("当前设备不支持 AI 增强") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } .padding(.top, 4) } @@ -472,7 +472,7 @@ struct EditorView: View { .font(.headline) Text("适用于较旧设备或生成失败时") .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } } } @@ -509,12 +509,12 @@ struct EditorView: View { .font(.caption) } } - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) .padding(.leading, 4) } } .padding(16) - .background(Color.secondary.opacity(0.1)) + .background(Color.softElevated) .clipShape(RoundedRectangle(cornerRadius: 12)) } @@ -543,7 +543,7 @@ struct EditorView: View { Text(suggestion.description) .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) if let actionText = suggestion.actionText { Button { @@ -804,7 +804,7 @@ struct AspectRatioButton: View { VStack(spacing: 4) { // 比例图标 RoundedRectangle(cornerRadius: 4) - .stroke(isSelected ? Color.accentColor : Color.secondary, lineWidth: 2) + .stroke(isSelected ? Color.accentColor : Color.textSecondary, lineWidth: 2) .frame(width: iconWidth, height: iconHeight) .background( isSelected ? Color.accentColor.opacity(0.1) : Color.clear @@ -814,14 +814,18 @@ struct AspectRatioButton: View { Text(template.displayName) .font(.caption2) .fontWeight(isSelected ? .semibold : .regular) - .foregroundStyle(isSelected ? .primary : .secondary) + .foregroundColor(isSelected ? .textPrimary : .textSecondary) } .frame(maxWidth: .infinity) - .padding(.vertical, 8) + .padding(.vertical, DesignTokens.Spacing.sm) .background(isSelected ? Color.accentColor.opacity(0.1) : Color.clear) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm)) } .buttonStyle(.plain) + .accessibilityElement(children: .ignore) + .accessibilityLabel(String(localized: "accessibility.aspectRatio \(template.displayName)")) + .accessibilityAddTraits(.isButton) + .accessibilityAddTraits(isSelected ? .isSelected : []) } private var iconWidth: CGFloat { @@ -957,7 +961,7 @@ struct ScaleButtonStyle: ButtonStyle { configuration.label .scaleEffect(configuration.isPressed ? 0.96 : 1.0) .opacity(configuration.isPressed ? 0.9 : 1.0) - .animation(.easeInOut(duration: 0.15), value: configuration.isPressed) + .animation(DesignTokens.Animation.quick, value: configuration.isPressed) } } diff --git a/to-live-photo/to-live-photo/Views/HomeView.swift b/to-live-photo/to-live-photo/Views/HomeView.swift index 9b0ad7c..fd14969 100644 --- a/to-live-photo/to-live-photo/Views/HomeView.swift +++ b/to-live-photo/to-live-photo/Views/HomeView.swift @@ -38,7 +38,7 @@ struct HomeView: View { .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - SoftIconButton("gearshape") { + SoftIconButton("gearshape", accessibilityLabel: String(localized: "accessibility.settings")) { appState.navigateTo(.settings) } } @@ -351,13 +351,17 @@ struct RecentWorkCard: View { } .buttonStyle(.plain) .scaleEffect(isPressed ? 0.97 : 1.0) - .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isPressed) + .animation(DesignTokens.Animation.spring, value: isPressed) .onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in isPressed = pressing }, perform: {}) .onAppear { thumbnailLoader.load(assetId: work.assetLocalIdentifier) } + .accessibilityElement(children: .ignore) + .accessibilityLabel(String(localized: "accessibility.livePhoto")) + .accessibilityHint(Text("\(work.aspectRatioDisplayName), \(work.createdAt.formatted(.relative(presentation: .named)))")) + .accessibilityAddTraits(.isButton) } } @@ -366,7 +370,7 @@ struct HomeButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.97 : 1.0) - .animation(.spring(response: 0.3, dampingFraction: 0.6), value: configuration.isPressed) + .animation(DesignTokens.Animation.spring, value: configuration.isPressed) } } diff --git a/to-live-photo/to-live-photo/Views/SettingsView.swift b/to-live-photo/to-live-photo/Views/SettingsView.swift index f9c367d..34f9c71 100644 --- a/to-live-photo/to-live-photo/Views/SettingsView.swift +++ b/to-live-photo/to-live-photo/Views/SettingsView.swift @@ -63,7 +63,7 @@ struct SettingsView: View { Label(String(localized: "settings.cacheSize"), systemImage: "internaldrive") Spacer() Text(cacheSize) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } Button(role: .destructive) { @@ -111,7 +111,7 @@ struct SettingsView: View { Label(String(localized: "settings.version"), systemImage: "info.circle") Spacer() Text(appVersion) - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) } NavigationLink { @@ -175,7 +175,7 @@ struct SettingsView: View { .labelStyle(.iconOnly) case .notDetermined: Label(String(localized: "settings.notDetermined"), systemImage: "questionmark.circle.fill") - .foregroundStyle(.secondary) + .foregroundColor(.textSecondary) .labelStyle(.iconOnly) @unknown default: EmptyView() diff --git a/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift b/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift index 64e9334..1c1d749 100644 --- a/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift +++ b/to-live-photo/to-live-photo/Views/WallpaperGuideView.swift @@ -171,7 +171,7 @@ struct WallpaperGuideView: View { ) } .padding(12) - .background(Color.secondary.opacity(0.1)) + .background(Color.softElevated) .clipShape(RoundedRectangle(cornerRadius: 12)) } } @@ -316,7 +316,7 @@ struct FAQRow: View { } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) - .background(Color.secondary.opacity(0.08)) + .background(Color.softElevated) .clipShape(RoundedRectangle(cornerRadius: 12)) } }