fix: UI 设计系统优化 - 无障碍、深色模式、对比度

- 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 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-03 23:15:41 +08:00
parent 143c471714
commit e041cacd7d
6 changed files with 199 additions and 47 deletions

View File

@@ -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<Double>
let gradient: LinearGradient
let accessibilityLabel: String
let step: Double
init(
value: Binding<Double>,
in range: ClosedRange<Double>,
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
}
}
}
}

View File

@@ -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"

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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))
}
}