a3f0b49112
Replace the stubbed chat API with a real WebSocket hook that connects to /ws/chat and supports DM creation, messaging, session join/leave, and history retrieval. Keep REST stub functions for backward compatibility with existing pages that require listChatSessions/getChatSessionById imports.
190 lines
6.5 KiB
TypeScript
190 lines
6.5 KiB
TypeScript
"use client"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card } from "@/components/ui/card"
|
|
import { EmptyState } from "@/components/ui/empty-state"
|
|
import { Input } from "@/components/ui/input"
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import { uploadFile } from "@/lib/api"
|
|
import { useChatSocket } from "@/lib/hooks/use-chat-socket"
|
|
import { notifyInfo } from "@/lib/toast"
|
|
import { cn } from "@/lib/utils"
|
|
import { useAuthStore } from "@/store/auth"
|
|
import { ArrowLeft, ImagePlus, MessageSquare, Send } from "lucide-react"
|
|
import Image from "next/image"
|
|
import Link from "next/link"
|
|
import { use, useEffect, useRef, useState } from "react"
|
|
|
|
export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
const { id: targetUserId } = use(params)
|
|
const [input, setInput] = useState("")
|
|
const [uploading, setUploading] = useState(false)
|
|
const imageInputRef = useRef<HTMLInputElement>(null)
|
|
const { user } = useAuthStore()
|
|
const {
|
|
connected,
|
|
sessionId,
|
|
messages,
|
|
error,
|
|
createDM,
|
|
leaveSession,
|
|
sendTextMessage,
|
|
sendImageMessage,
|
|
} = useChatSocket()
|
|
|
|
useEffect(() => {
|
|
if (!connected) return
|
|
createDM(targetUserId)
|
|
}, [connected, createDM, targetUserId])
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (sessionId) leaveSession(sessionId)
|
|
},
|
|
[leaveSession, sessionId],
|
|
)
|
|
|
|
if (!user?.id) {
|
|
return (
|
|
<div className="container mx-auto max-w-2xl px-4 py-8">
|
|
<EmptyState
|
|
title="请先登录"
|
|
description="登录后可查看会话并发送消息。"
|
|
icon={MessageSquare}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto flex h-[calc(100vh-3.5rem)] max-w-2xl flex-col px-4 py-8">
|
|
<Card className="flex flex-1 flex-col overflow-hidden border-border/80 shadow-sm">
|
|
<div className="flex items-center gap-3 border-b border-border/60 bg-background px-4 py-3">
|
|
<Link href="/chat" className="text-muted-foreground hover:text-foreground">
|
|
<ArrowLeft className="h-5 w-5" />
|
|
</Link>
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
<MessageSquare className="h-4 w-4" />
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium">会话 {targetUserId}</span>
|
|
<p className="text-xs text-muted-foreground">
|
|
{connected ? (sessionId ? "已连接" : "正在创建会话") : "连接中"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<ScrollArea className="flex-1 p-4">
|
|
<div className="space-y-4">
|
|
{error && <p className="text-center text-xs text-destructive">{error}</p>}
|
|
{messages.length === 0 && (
|
|
<EmptyState
|
|
title="暂无消息"
|
|
description="发送第一条消息开始沟通。"
|
|
icon={MessageSquare}
|
|
/>
|
|
)}
|
|
{messages.map((msg) => {
|
|
if (msg.type === "system") {
|
|
return (
|
|
<div key={msg.id} className="text-center">
|
|
<span className="rounded-full bg-muted/60 px-2 py-1 text-xs text-muted-foreground">
|
|
{msg.content}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
const isMine = msg.senderId === user.id
|
|
return (
|
|
<div key={msg.id} className={cn("flex gap-2", isMine && "flex-row-reverse")}>
|
|
<div className={cn("max-w-[70%]", isMine && "text-right")}>
|
|
{msg.type === "image" ? (
|
|
<Image
|
|
src={msg.content}
|
|
alt="聊天图片"
|
|
width={256}
|
|
height={192}
|
|
unoptimized
|
|
className="inline-block max-h-48 max-w-64 rounded-lg border"
|
|
/>
|
|
) : (
|
|
<p
|
|
className={cn(
|
|
"inline-block rounded-xl border px-3 py-2 text-sm",
|
|
isMine
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
: "border-border/60 bg-muted/60",
|
|
)}
|
|
>
|
|
{msg.content}
|
|
</p>
|
|
)}
|
|
<p className="mt-1 text-[10px] text-muted-foreground">
|
|
{new Date(msg.createdAt).toLocaleTimeString("zh-CN", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
<div className="border-t border-border/60 bg-background p-4">
|
|
<input
|
|
ref={imageInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={(event) => {
|
|
const target = event.currentTarget
|
|
const file = target.files?.[0]
|
|
if (!file) return
|
|
|
|
setUploading(true)
|
|
void uploadFile(file, "chat")
|
|
.then((imageUrl) => sendImageMessage(imageUrl))
|
|
.catch(() => notifyInfo("图片发送失败"))
|
|
.finally(() => {
|
|
setUploading(false)
|
|
target.value = ""
|
|
})
|
|
}}
|
|
/>
|
|
<form
|
|
className="flex gap-2"
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
const text = input.trim()
|
|
if (!text || !sessionId) return
|
|
sendTextMessage(text)
|
|
setInput("")
|
|
}}
|
|
>
|
|
<Input
|
|
value={input}
|
|
onChange={(event) => setInput(event.target.value)}
|
|
placeholder="输入消息..."
|
|
className="flex-1 border-border/60"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
size="icon"
|
|
variant="outline"
|
|
disabled={!sessionId || uploading}
|
|
onClick={() => imageInputRef.current?.click()}
|
|
>
|
|
<ImagePlus className="h-4 w-4" />
|
|
</Button>
|
|
<Button type="submit" size="icon" disabled={!sessionId || !input.trim()}>
|
|
<Send className="h-4 w-4" />
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|