wangdl f6df01d9ca feat(ios): TabBar 显示/隐藏增加入场出场动画
- TabBarState ObservableObject 管理可见性
- ContentView 用 .toolbar(hidden)+animation 驱动动画
- 子页面 hideTabBarWithAnimation() 替代静态 toolbar hidden
- 0.28s easeInOut 淡入淡出

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:57:39 +08:00

128 lines
7.5 KiB
Swift

import SwiftUI
import UIKit
struct AIChatPage: View {
@StateObject private var vm = AIChatViewModel()
@State private var showSessions = false
@State private var sessions: [ChatSession] = []
var body: some View {
ZStack {
Color.zxBg0.ignoresSafeArea()
VStack(spacing: 0) {
if vm.isCreatingSession {
VStack(spacing: 12) {
ProgressView().tint(Color.zxPurple)
Text("正在准备 AI 助手...").font(.system(size: 13)).foregroundColor(Color.zxF04)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = vm.sessionError {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle").font(.system(size: 36)).foregroundColor(Color.zxF04)
Text(error).font(.system(size: 14)).foregroundColor(Color.zxF04)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 16) {
ForEach(vm.messages) { m in
VStack(alignment: m.role == .user ? .trailing : .leading, spacing: 6) {
chatBubble(m)
// Citations below AI messages
if m.role == .ai, let citations = m.citations, !citations.isEmpty {
VStack(spacing: 4) {
ForEach(citations.prefix(3)) { c in
HStack(spacing: 4) {
Image(systemName: "doc.text").font(.system(size: 9)).foregroundColor(Color.zxF04)
Text(c.excerptText?.prefix(60).description ?? "").font(.system(size: 10)).foregroundColor(Color.zxF04).lineLimit(1)
}.padding(.horizontal, 8).padding(.vertical, 4)
.background(Color.zxFill004).clipShape(Capsule())
}
}.padding(.leading, 36)
}
// Actions for AI messages
if m.role == .ai {
HStack(spacing: 16) {
Button { UIPasteboard.general.string = m.content } label: {
Label("复制", systemImage: "doc.on.doc").font(.system(size: 11)).foregroundColor(Color.zxF04)
}
Button { Task { vm.send() } } label: {
Label("重新生成", systemImage: "arrow.clockwise").font(.system(size: 11)).foregroundColor(Color.zxF04)
}
}.padding(.leading, 36)
}
}.id(m.id).padding(.horizontal, 20)
}
if vm.isSending {
HStack(spacing: 8) {
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
.frame(width: 28, height: 28).background(Color(hex: "#7C6EFA", opacity: 0.15)).clipShape(Circle())
ZXDotLoader(color: Color.zxPurple).padding(.leading, 4); Spacer()
}.padding(.horizontal, 20)
}
}.padding(.top, 8).padding(.bottom, 100)
}
.scrollIndicators(.hidden)
.onChange(of: vm.messages.count) { _ in withAnimation { proxy.scrollTo(vm.messages.last?.id) } }
}
ZXAIInputBar(text: $vm.inputText, onSend: { vm.send() }).padding(.horizontal, 20).padding(.bottom, 34)
}
}
}
.navigationBarTitleDisplayMode(.inline).hideTabBarWithAnimation()
.toolbarBackground(.hidden, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button { Task { sessions = (try? await RagChatService.shared.listSessions()) ?? []; showSessions = true } } label: {
Image(systemName: "list.bullet.rectangle").font(.system(size: 16)).foregroundColor(Color.zxF05)
}
}
}
.sheet(isPresented: $showSessions) {
VStack(spacing: 0) {
RoundedRectangle(cornerRadius: 3).fill(Color.zxF03).frame(width: 36, height: 5).padding(.top, 12).padding(.bottom, 16)
Text("对话列表").font(.system(size: 16, weight: .semibold)).foregroundColor(Color.zxF0).padding(.bottom, 16)
if sessions.isEmpty {
Text("暂无历史对话").font(.system(size: 14)).foregroundColor(Color.zxF04).padding(.top, 40)
} else {
ScrollView {
VStack(spacing: 0) {
ForEach(sessions) { s in
Button {
showSessions = false
Task { await vm.loadSession(s.id) }
} label: {
HStack {
Text(s.title ?? "对话").font(.system(size: 14)).foregroundColor(Color.zxF0)
Spacer()
Text(s.updatedAt?.prefix(10).description ?? "").font(.system(size: 11)).foregroundColor(Color.zxF04)
}.padding(.horizontal, 20).padding(.vertical, 14)
}.foregroundColor(.primary)
}
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.zxBg0)
.presentationDetents([.medium, .large])
}
.task { await vm.load() }
}
private func chatBubble(_ m: AIMessage) -> some View {
HStack(alignment: .top, spacing: 8) {
if m.role == .ai {
Image(systemName: "brain.head.profile").foregroundColor(Color.zxPurple)
.frame(width: 28, height: 28).background(Color(hex: "#7C6EFA", opacity: 0.15)).clipShape(Circle())
}
Text(m.content).zxFontScaled(size: 14)
.foregroundColor(m.role == .user ? .white : Color.zxF007).padding(12)
.background(m.role == .user ? AnyView(ZXGradient.brandPurple) : AnyView(Color.zxFill004))
.clipShape(RoundedRectangle(cornerRadius: 16))
if m.role == .user {
Circle().frame(width: 28, height: 28)
.foregroundColor(Color.zxPurpleBG(0.2)).overlay(Text("").font(.system(size: 10, weight: .bold)).foregroundColor(Color.zxPurple))
}
}.frame(maxWidth: .infinity, alignment: m.role == .user ? .trailing : .leading)
}
}