114 行
3.9 KiB
Swift
114 行
3.9 KiB
Swift
|
|
import SwiftUI
|
||
|
|
import XuqmSDK
|
||
|
|
|
||
|
|
struct ChatView: View {
|
||
|
|
let targetId: String
|
||
|
|
let targetName: String
|
||
|
|
let currentUserId: String
|
||
|
|
|
||
|
|
@StateObject private var viewModel = ChatViewModel()
|
||
|
|
@State private var scrollToBottom = false
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
VStack(spacing: 0) {
|
||
|
|
if viewModel.connectionStatus != "已连接" {
|
||
|
|
Text(viewModel.connectionStatus)
|
||
|
|
.font(.caption)
|
||
|
|
.foregroundStyle(.white)
|
||
|
|
.padding(.vertical, 4)
|
||
|
|
.frame(maxWidth: .infinity)
|
||
|
|
.background(viewModel.connectionStatus.contains("错误") ? Color.red : Color.orange)
|
||
|
|
}
|
||
|
|
|
||
|
|
ScrollViewReader { proxy in
|
||
|
|
ScrollView {
|
||
|
|
LazyVStack(spacing: 8) {
|
||
|
|
ForEach(viewModel.messages.reversed(), id: \.id) { message in
|
||
|
|
MessageBubble(message: message, currentUserId: currentUserId)
|
||
|
|
.id(message.id)
|
||
|
|
.rotationEffect(.degrees(180))
|
||
|
|
}
|
||
|
|
|
||
|
|
if viewModel.isLoading {
|
||
|
|
ProgressView()
|
||
|
|
.padding()
|
||
|
|
.rotationEffect(.degrees(180))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.padding()
|
||
|
|
.rotationEffect(.degrees(180))
|
||
|
|
}
|
||
|
|
.onChange(of: viewModel.messages.count) { _ in
|
||
|
|
if let first = viewModel.messages.first {
|
||
|
|
withAnimation {
|
||
|
|
proxy.scrollTo(first.id, anchor: .bottom)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Divider()
|
||
|
|
|
||
|
|
HStack(spacing: 12) {
|
||
|
|
TextField("输入消息…", text: $viewModel.inputText, axis: .vertical)
|
||
|
|
.textFieldStyle(.roundedBorder)
|
||
|
|
.lineLimit(1...4)
|
||
|
|
|
||
|
|
Button {
|
||
|
|
viewModel.sendText()
|
||
|
|
} label: {
|
||
|
|
Image(systemName: "paperplane.fill")
|
||
|
|
.foregroundStyle(viewModel.inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? Color.gray : Color.accentColor)
|
||
|
|
}
|
||
|
|
.disabled(viewModel.inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||
|
|
}
|
||
|
|
.padding()
|
||
|
|
}
|
||
|
|
.navigationTitle(targetName)
|
||
|
|
#if os(iOS)
|
||
|
|
.navigationBarTitleDisplayMode(.inline)
|
||
|
|
#endif
|
||
|
|
.onAppear {
|
||
|
|
viewModel.setup(targetId: targetId, chatType: .single)
|
||
|
|
viewModel.markRead()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct MessageBubble: View {
|
||
|
|
let message: ImMessage
|
||
|
|
let currentUserId: String
|
||
|
|
|
||
|
|
private var isOwn: Bool {
|
||
|
|
message.fromUserId == currentUserId
|
||
|
|
}
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
HStack {
|
||
|
|
if isOwn { Spacer(minLength: 40) }
|
||
|
|
|
||
|
|
VStack(alignment: isOwn ? .trailing : .leading, spacing: 2) {
|
||
|
|
Text(parseMessageText(message))
|
||
|
|
.padding(.horizontal, 12)
|
||
|
|
.padding(.vertical, 8)
|
||
|
|
.background(isOwn ? Color.accentColor.opacity(0.15) : Color.gray.opacity(0.2))
|
||
|
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||
|
|
.foregroundStyle(.primary)
|
||
|
|
|
||
|
|
HStack(spacing: 4) {
|
||
|
|
if isOwn {
|
||
|
|
Text(statusLabel(message.status))
|
||
|
|
.font(.caption2)
|
||
|
|
.foregroundStyle(.secondary)
|
||
|
|
}
|
||
|
|
Text(formatConversationTime(message.createdAt))
|
||
|
|
.font(.caption2)
|
||
|
|
.foregroundStyle(.secondary)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !isOwn { Spacer(minLength: 40) }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|