Files
juwan-frontend/app/(order)/chat/[id]/page.tsx
T

203 lines
6.9 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,
joinSession,
leaveSession,
sendTextMessage,
sendImageMessage,
} = useChatSocket()
const cacheKey = user?.id ? `chat:dm:${user.id}:${targetUserId}` : null
useEffect(() => {
if (!connected || !cacheKey) return
const cached = window.localStorage.getItem(cacheKey)
if (cached) {
joinSession(cached)
} else {
createDM(targetUserId)
}
}, [connected, cacheKey, createDM, joinSession, targetUserId])
useEffect(() => {
if (!sessionId || !cacheKey) return
window.localStorage.setItem(cacheKey, sessionId)
}, [sessionId, cacheKey])
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>
)
}