wangdl 9f4e3815d4 feat(ios): 播放/暂停/下载/上传/左右箭头 6 个图标全部替换
- icon-play / icon-pause / icon-download / icon-upload
- icon-chevron-left / icon-chevron-right
- 全局替换 systemName: chevron/play 引用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:38:15 +08:00

483 lines
17 KiB
Swift

//
// AIHomeView.swift AI v2.0
// #3D7FFB, #3D7FFB #9DA7FD,
//
import SwiftUI
// MARK: - Rive Animation Placeholder
/// Rive Rive
struct ZXRivePlaceholder: View {
let height: CGFloat
let label: String
let icon: String
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: ZXRadius.lg)
.fill(
LinearGradient(
colors: [Color.zxPrimarySoft, Color.zxPrimarySoft.opacity(0.3)],
startPoint: .top, endPoint: .bottom
)
)
.overlay(
RoundedRectangle(cornerRadius: ZXRadius.lg)
.stroke(Color.zxPrimary.opacity(0.12), lineWidth: 1)
)
VStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 36))
.foregroundColor(Color.zxPrimary.opacity(0.4))
.symbolEffect(.pulse, options: .repeating.speed(0.5))
Text(label)
.font(.system(size: 13, weight: .medium))
.foregroundColor(Color.zxInkSecondary)
}
}
.frame(height: height)
}
}
// MARK: - AI Home View
struct AIHomeView: View {
@State private var text = ""
@State private var serverStatus: ServerStatus = .checking
@State private var serverMessage = ""
@State private var navigateToChat = false
enum ServerStatus { case checking, online, offline }
var body: some View {
ZStack {
//
Color.zxCanvas.ignoresSafeArea()
//
Circle()
.fill(RadialGradient(
colors: [Color.zxPrimarySoft.opacity(0.6), .clear],
center: .top, startRadius: 0, endRadius: 280
))
.frame(width: 280, height: 280)
.offset(y: -60)
.allowsHitTesting(false)
VStack(spacing: 0) {
// MARK: -
topBar
.padding(.horizontal, ZXSpacing.pageHPadding)
.padding(.top, ZXSpacing.statusBarH + 12)
ScrollView {
VStack(spacing: ZXSpacing.xxl) {
// MARK: - Hero
heroSection
// MARK: - Rive
riveSection
// MARK: -
quickActionsSection
// MARK: -
thinkingCardSection
// MARK: - AI
suggestionSection
}
.padding(.horizontal, ZXSpacing.pageHPadding)
.padding(.bottom, 120)
}
.scrollIndicators(.hidden)
// MARK: -
inputBar
}
NavigationLink(destination: AIChatPage(), isActive: $navigateToChat) { EmptyView() }
}
.navigationDestination(for: Route.self) { $0.destination }
.task { await checkServer() }
}
// MARK: - Server Check
private func checkServer() async {
serverStatus = .checking
do {
struct HealthResponse: Decodable { let status: String }
let resp: HealthResponse = try await APIClient.shared.request("/")
serverStatus = .online
serverMessage = resp.status
} catch {
serverStatus = .offline
serverMessage = ""
}
}
// MARK: - Top Bar
private var topBar: some View {
HStack {
//
VStack(alignment: .leading, spacing: 4) {
Text(formattedDate)
.font(.system(size: ZXFont.caption2.size, weight: .medium))
.foregroundColor(Color.zxInkTertiary)
.textCase(.uppercase)
HStack(spacing: 6) {
Circle()
.fill(serverStatus == .online ? Color.zxMint
: serverStatus == .checking ? Color.zxAmber
: Color.zxCoral)
.frame(width: 6, height: 6)
Text(serverStatusLabel)
.font(.system(size: ZXFont.caption.size, weight: .medium))
.foregroundColor(serverStatus == .online ? Color.zxMintDeep
: serverStatus == .checking ? Color.zxAmberDeep
: Color.zxCoralDeep)
}
}
Spacer()
//
Button { Task { await checkServer() } } label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 16, weight: .medium))
.foregroundColor(Color.zxInkSecondary)
.frame(width: 40, height: 40)
.background(Color.zxSurface)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.md))
}
}
}
// MARK: - Hero Section
private var heroSection: some View {
VStack(alignment: .leading, spacing: 10) {
// Tag
HStack(spacing: 6) {
Image(systemName: "sparkles")
.font(.system(size: 11))
Text("AI 驱动学习")
.font(.system(size: ZXFont.caption2.size, weight: .semibold))
}
.foregroundColor(Color.zxPrimary)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(Color.zxPrimarySoft)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.xs))
//
Text("用 AI 重新定义\n你的学习方式")
.font(.system(
size: ZXFont.hero.size,
weight: ZXFont.hero.weight
))
.tracking(ZXFont.hero.spacing)
.foregroundColor(Color.zxInkPrimary)
.lineSpacing(4)
//
Text("智能导入 · 主动回忆 · 间隔复习 · AI 诊断")
.font(.system(size: ZXFont.bodySmall.size, weight: .regular))
.foregroundColor(Color.zxInkSecondary)
.padding(.top, 2)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
// MARK: - Rive Section
private var riveSection: some View {
ZXRivePlaceholder(
height: 180,
label: "Rive 动画 — AI 学习示意",
icon: "sparkles"
)
}
// MARK: - Quick Actions
private var quickActionsSection: some View {
VStack(alignment: .leading, spacing: ZXSpacing.md) {
Text("快速操作")
.font(.system(size: ZXFont.caption2.size, weight: .semibold))
.foregroundColor(Color.zxInkTertiary)
.textCase(.uppercase)
.tracking(0.5)
HStack(spacing: 12) {
NavigationLink(value: Route.activeRecall) {
quickActionItem(
icon: "brain.head.profile",
label: "生成\n回忆测试"
)
}
.foregroundColor(.primary)
NavigationLink(value: Route.weakPoints) {
quickActionItem(
icon: "exclamationmark.triangle",
label: "分析\n薄弱点"
)
}
.foregroundColor(.primary)
NavigationLink(value: Route.aiChat) {
quickActionItem(
icon: "mic.fill",
label: "费曼\n解释练习"
)
}
.foregroundColor(.primary)
NavigationLink(value: Route.reviewCard) {
quickActionItem(
icon: "calendar",
label: "今日\n复习计划"
)
}
.foregroundColor(.primary)
}
}
}
private func quickActionItem(icon: String, label: String) -> some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 20, weight: .regular))
.foregroundColor(Color.zxPrimary)
.frame(width: 42, height: 42)
.background(Color.zxPrimarySoft)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.md))
Text(label)
.font(.system(size: 11, weight: .medium))
.foregroundColor(Color.zxInkSecondary)
.multilineTextAlignment(.center)
.lineSpacing(3)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.zxSurfaceElevated)
.overlay(
RoundedRectangle(cornerRadius: ZXRadius.lg)
.stroke(Color.zxHairline, lineWidth: 0.5)
)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.lg))
.accessibilityLabel(label.replacingOccurrences(of: "\n", with: ""))
}
// MARK: - Thinking Card
private var thinkingCardSection: some View {
VStack(alignment: .leading, spacing: ZXSpacing.md) {
Text("今日思考")
.font(.system(size: ZXFont.caption2.size, weight: .semibold))
.foregroundColor(Color.zxInkTertiary)
.textCase(.uppercase)
.tracking(0.5)
VStack(alignment: .leading, spacing: 14) {
// Header
HStack {
Image(systemName: "sparkles")
.font(.system(size: 13))
.foregroundColor(Color.zxPrimary)
.frame(width: 30, height: 30)
.background(Color.zxPrimarySoft)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.sm))
Text("每日思考题")
.font(.system(size: ZXFont.callout.size, weight: .semibold))
.foregroundColor(Color.zxInkPrimary)
Spacer()
Text("待回答")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(Color.zxAmberDeep)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(Color.zxAmberSoft)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.xs))
}
// Question
Text("解释「注意力机制」在 Transformer 中的作用,不能使用搜索,用你自己的话说。")
.font(.system(size: ZXFont.bodySmall.size, weight: .regular))
.foregroundColor(Color.zxInkPrimary)
.lineSpacing(6)
// Action
NavigationLink(value: Route.dailyThinking) {
HStack(spacing: 8) {
Text("开始回答")
.font(.system(size: ZXFont.callout.size, weight: .semibold))
Image(systemName: "arrow.right")
.font(.system(size: 12, weight: .semibold))
}
.foregroundColor(Color.zxOnPrimary)
.frame(maxWidth: .infinity)
.frame(height: 48)
.background(ZXGradient.brand)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.md))
}
.accessibilityLabel("开始回答今日思考题")
.accessibilityHint("用费曼方法解释注意力机制")
}
.padding(ZXSpacing.lg)
.background(Color.zxSurfaceElevated)
.overlay(
RoundedRectangle(cornerRadius: ZXRadius.lg)
.stroke(Color.zxHairline, lineWidth: 0.5)
)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.lg))
}
}
// MARK: - Suggestions
private var suggestionSection: some View {
VStack(alignment: .leading, spacing: ZXSpacing.md) {
HStack {
Text("你可以问 AI")
.font(.system(size: ZXFont.caption2.size, weight: .semibold))
.foregroundColor(Color.zxInkTertiary)
.textCase(.uppercase)
.tracking(0.5)
Spacer()
Text("查看全部")
.font(.system(size: 13, weight: .medium))
.foregroundColor(Color.zxPrimary)
}
VStack(spacing: 8) {
suggestionRow("\"帮我测试机器学习这章的掌握情况\"", icon: "doc.text")
suggestionRow("\"我最近的薄弱知识点有哪些?\"", icon: "chart.bar")
suggestionRow("\"生成一份本周的复习计划\"", icon: "calendar.badge.plus")
}
}
}
private func suggestionRow(_ text: String, icon: String) -> some View {
Button {
self.text = text
navigateToChat = true
} label: {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.system(size: 13))
.foregroundColor(Color.zxInkTertiary)
.frame(width: 32, height: 32)
.background(Color.zxSurface)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.sm))
Text(text)
.font(.system(size: ZXFont.bodySmall.size, weight: .regular))
.foregroundColor(Color.zxInkPrimary)
Spacer()
Image("icon-chevron-right")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(Color.zxInkTertiary)
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
.background(Color.zxSurfaceElevated)
.overlay(
RoundedRectangle(cornerRadius: ZXRadius.lg)
.stroke(Color.zxHairline, lineWidth: 0.5)
)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.lg))
}
}
// MARK: - Bottom Input Bar
private var inputBar: some View {
HStack(spacing: 10) {
Image(systemName: "sparkles")
.font(.system(size: 15))
.foregroundColor(Color.zxPrimary)
TextField("问 AI 任何学习问题…", text: $text)
.font(.system(size: ZXFont.bodySmall.size))
.tint(Color.zxPrimary)
Spacer()
//
Button {} label: {
Image(systemName: "mic.fill")
.font(.system(size: 17))
.foregroundColor(Color.zxInkTertiary)
}
//
Button {
navigateToChat = true
} label: {
Image(systemName: "arrow.up")
.font(.system(size: 14, weight: .bold))
.foregroundColor(Color.zxOnPrimary)
.frame(width: 32, height: 32)
.background(
text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? AnyView(Color.zxHairlineStrong)
: AnyView(ZXGradient.brand)
)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.sm))
}
.disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
.accessibilityLabel("发送消息")
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(.ultraThinMaterial)
.background(Color.zxSurfaceElevated)
.overlay(
RoundedRectangle(cornerRadius: ZXRadius.md)
.stroke(Color.zxHairline, lineWidth: 0.5)
)
.clipShape(RoundedRectangle(cornerRadius: ZXRadius.md))
.shadow(color: .black.opacity(0.04), radius: 8, y: -2)
.padding(.horizontal, ZXSpacing.pageHPadding)
.padding(.bottom, ZXSpacing.tabBarH + ZXSpacing.lg)
}
// MARK: - Helpers
private var formattedDate: String {
let f = DateFormatter()
f.locale = Locale(identifier: "zh_CN")
f.dateFormat = "M 月 d 日 EEEE"
return f.string(from: Date())
}
private var serverStatusLabel: String {
switch serverStatus {
case .online: return "服务在线"
case .checking: return "检测中…"
case .offline: return "离线"
}
}
}
// MARK: - Navigation stub (unchanged)
#Preview {
AIHomeView()
.preferredColorScheme(.light)
}