import SwiftUI // MARK: - Animated Button Style struct ZXButtonStyle: ButtonStyle { let branded: Bool init(branded: Bool = false) { self.branded = branded } func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.96 : 1.0) .opacity(configuration.isPressed ? 0.85 : 1.0) .animation(.easeOut(duration: 0.15), value: configuration.isPressed) .sensoryFeedback(.impact(weight: .light), trigger: configuration.isPressed) } } // MARK: - Scale press modifier (quick apply) struct ZXPressModifier: ViewModifier { @State private var pressed = false func body(content: Content) -> some View { content .scaleEffect(pressed ? 0.96 : 1.0) .opacity(pressed ? 0.8 : 1.0) .animation(.easeOut(duration: 0.12), value: pressed) .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in pressed = true } .onEnded { _ in pressed = false } ) .sensoryFeedback(.impact(weight: .light), trigger: pressed) } } extension View { func zxPressable() -> some View { modifier(ZXPressModifier()) } } // MARK: - Page transition modifier struct ZXPageTransition: ViewModifier { let edge: Edge func body(content: Content) -> some View { content .transition( .move(edge: edge) .combined(with: .opacity) ) } } extension View { func zxPageTransition(from edge: Edge = .trailing) -> some View { modifier(ZXPageTransition(edge: edge)) } } // MARK: - AI Thinking overlay struct ZXThinkingOverlay: View { let message: String init(_ message: String = "AI 正在分析你的回答…") { self.message = message } @State private var show = false var body: some View { ZStack { Color.black.opacity(0.4).ignoresSafeArea() VStack(spacing: 20) { // Animated brain ZStack { Circle() .fill(RadialGradient( colors: [Color(hex: "#7C6EFA", opacity: 0.3), .clear], center: .center, startRadius: 8, endRadius: 32 )) .frame(width: 64, height: 64) .scaleEffect(show ? 1.3 : 0.8) .animation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: show) Image(systemName: "brain.head.profile") .font(.system(size: 28)) .foregroundColor(.white.opacity(0.9)) } VStack(spacing: 12) { Text(message) .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) ZXDotLoader(color: .white) } } .padding(32) .background( RoundedRectangle(cornerRadius: 24) .fill(.ultraThinMaterial) ) } .onAppear { show = true } } } // MARK: - Celebration / Confetti effect struct ZXCelebrationView: View { let title: String let subtitle: String let onDismiss: () -> Void @State private var particles: [ConfettiParticle] = [] @State private var showContent = false var body: some View { ZStack { Color.black.opacity(0.5).ignoresSafeArea() .onTapGesture { dismiss() } // Particles ForEach(particles) { p in Circle() .fill(p.color) .frame(width: p.size, height: p.size) .position(x: p.x, y: p.y) .opacity(p.opacity) .scaleEffect(p.scale) } // Content card VStack(spacing: 20) { ZStack { Circle() .fill( LinearGradient( colors: [Color(hex: "#7C6EFA"), Color(hex: "#F97316")], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 80, height: 80) Image(systemName: "sparkles") .font(.system(size: 36)) .foregroundColor(.white) } .scaleEffect(showContent ? 1 : 0.5) .animation(.spring(response: 0.5, dampingFraction: 0.6).delay(0.2), value: showContent) VStack(spacing: 6) { Text(title) .font(.system(size: 22, weight: .heavy)) .foregroundColor(.white) Text(subtitle) .font(.system(size: 14)) .foregroundColor(Color(hex: "#F0F0FF", opacity: 0.6)) } .opacity(showContent ? 1 : 0) .offset(y: showContent ? 0 : 20) Button(action: dismiss) { Text("继续学习") .font(.system(size: 16, weight: .bold)) .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: 52) .background( LinearGradient( colors: [Color(hex: "#7C6EFA"), Color(hex: "#F97316")], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .clipShape(RoundedRectangle(cornerRadius: 16)) } .opacity(showContent ? 1 : 0) } .padding(28) .background( RoundedRectangle(cornerRadius: 24) .fill(.ultraThinMaterial) ) .padding(.horizontal, 20) } .onAppear { showContent = true launchConfetti() } } private func dismiss() { withAnimation(.easeOut(duration: 0.25)) { showContent = false } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { onDismiss() } } private func launchConfetti() { let colors: [Color] = [Color(hex: "#7C6EFA"), Color(hex: "#F97316"), Color(hex: "#A78BFA"), Color(hex: "#34D399"), Color(hex: "#F59E0B"), Color(hex: "#4ECDC4")] var ps: [ConfettiParticle] = [] for i in 0..<60 { let delay = Double(i) * 0.015 let x = CGFloat.random(in: 0...UIScreen.main.bounds.width) let endY = CGFloat.random(in: 80...UIScreen.main.bounds.height * 0.7) let size = CGFloat.random(in: 4...10) let color = colors.randomElement()! ps.append(ConfettiParticle( id: UUID(), color: color, size: size, x: x, y: -30, targetY: endY, scale: 1, opacity: 1, delay: delay )) } particles = ps for p in particles { withAnimation(.spring(response: 0.8, dampingFraction: 0.6).delay(p.delay)) { if let idx = particles.firstIndex(where: { $0.id == p.id }) { particles[idx].y = p.targetY particles[idx].opacity = 0.4 particles[idx].scale = 0.3 } } } } } private struct ConfettiParticle: Identifiable { let id: UUID let color: Color let size: CGFloat var x: CGFloat var y: CGFloat let targetY: CGFloat var scale: CGFloat var opacity: Double let delay: Double } // MARK: - AI Analysis progress view struct ZXAIAnalysisProgress: View { let steps: [String] @State private var currentStep = 0 @State private var progress: CGFloat = 0 var body: some View { VStack(spacing: 24) { ZStack { ZXLoadingView(size: 48, lineWidth: 3) } VStack(spacing: 4) { Text("AI 分析中…") .font(.system(size: 17, weight: .bold)) .foregroundColor(Color.zxF0) Text(steps[safe: currentStep] ?? steps.last ?? "") .font(.system(size: 13)) .foregroundColor(Color.zxF04) } GeometryReader { g in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3) .fill(Color.zxFill008) .frame(height: 6) RoundedRectangle(cornerRadius: 3) .fill(ZXGradient.progressBar) .frame(width: g.size.width * progress, height: 6) .animation(.easeInOut(duration: 0.6), value: progress) } } .frame(height: 6) .padding(.horizontal, 40) } .padding(28) .background( RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) ) .padding(.horizontal, 40) .onAppear { var delay: TimeInterval = 0.8 for i in 0.. Element? { indices.contains(index) ? self[index] : nil } }