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.
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
"use client"
|
|
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { EmptyState } from "@/components/ui/empty-state"
|
|
import { getPlayerById, listOrders } from "@/lib/api"
|
|
import { isActiveOrder } from "@/lib/domain/order-filters"
|
|
import type { UserRole } from "@/lib/types"
|
|
import { useAuthStore } from "@/store/auth"
|
|
import { MessageSquare } from "lucide-react"
|
|
import Link from "next/link"
|
|
import { useEffect, useState } from "react"
|
|
|
|
type ChatEntry = {
|
|
orderId: string
|
|
targetUserId: string
|
|
title: string
|
|
description: string
|
|
}
|
|
|
|
function orderRole(role: UserRole): "consumer" | "player" | undefined {
|
|
if (role === "consumer" || role === "player") return role
|
|
return undefined
|
|
}
|
|
|
|
export default function ChatListPage() {
|
|
const currentRole = useAuthStore((state) => state.currentRole)
|
|
const role = orderRole(currentRole)
|
|
const [entries, setEntries] = useState<ChatEntry[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
void (async () => {
|
|
if (!role) {
|
|
setEntries([])
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
try {
|
|
const orders = (await listOrders({ role })).filter((order) => isActiveOrder(order.status))
|
|
const nextEntries = await Promise.all(
|
|
orders.map(async (order) => {
|
|
if (role === "consumer") {
|
|
const player = await getPlayerById(String(order.playerId))
|
|
if (!player) return null
|
|
return {
|
|
orderId: String(order.id),
|
|
targetUserId: player.user.id,
|
|
title: player.user.nickname,
|
|
description: order.service.title,
|
|
}
|
|
}
|
|
|
|
return {
|
|
orderId: String(order.id),
|
|
targetUserId: String(order.consumerId),
|
|
title: `客户 ${order.consumerId}`,
|
|
description: order.service.title,
|
|
}
|
|
}),
|
|
)
|
|
if (!cancelled)
|
|
setEntries(nextEntries.filter((entry): entry is ChatEntry => entry !== null))
|
|
} catch {
|
|
if (!cancelled) setEntries([])
|
|
} finally {
|
|
if (!cancelled) setLoading(false)
|
|
}
|
|
})()
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [role])
|
|
|
|
return (
|
|
<div className="container mx-auto max-w-2xl space-y-6 px-4 py-8">
|
|
<h1 className="text-2xl font-bold">消息</h1>
|
|
|
|
{entries.length > 0 ? (
|
|
<div className="overflow-hidden rounded-xl border border-border/80 bg-card shadow-sm">
|
|
{entries.map((entry) => (
|
|
<Link
|
|
key={entry.orderId}
|
|
href={`/chat/${entry.targetUserId}?orderId=${entry.orderId}`}
|
|
className="block border-b border-border/60 transition-colors last:border-0 hover:bg-muted/10"
|
|
>
|
|
<div className="flex items-center gap-3 p-4">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
<MessageSquare className="h-4 w-4" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium">{entry.title}</span>
|
|
<Badge variant="info" className="px-1.5 py-0 text-[10px] font-normal">
|
|
订单
|
|
</Badge>
|
|
</div>
|
|
<p className="truncate text-xs text-muted-foreground">{entry.description}</p>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<EmptyState
|
|
title={loading ? "消息加载中" : "暂无消息"}
|
|
description="进行中的订单沟通会显示在这里。"
|
|
icon={MessageSquare}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|