feat(ui): refine order chat pages

This commit is contained in:
zetaloop
2026-04-25 21:15:43 +08:00
parent b0cecd58b0
commit 8e02c8ca97
4 changed files with 115 additions and 75 deletions
+36 -16
View File
@@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import { EmptyState } from "@/components/ui/empty-state"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { getChatSessionById, listChatMessages } from "@/lib/api" import { getChatSessionById, listChatMessages } from "@/lib/api"
@@ -11,7 +12,7 @@ import { sendImageMessage, sendTextMessage } from "@/lib/api/chat"
import { notifyInfo } from "@/lib/toast" import { notifyInfo } from "@/lib/toast"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { ArrowLeft, ImagePlus, Send } from "lucide-react" import { ArrowLeft, ImagePlus, MessageSquare, Send } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { use, useEffect, useRef, useState } from "react" import { use, useEffect, useRef, useState } from "react"
@@ -55,22 +56,32 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
if (loading) { if (loading) {
return ( return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">...</div> <div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState title="加载中" description="正在读取会话内容..." icon={MessageSquare} />
</div>
) )
} }
if (!session) { if (!session) {
return ( return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground"> <div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState
title="会话不存在"
description="该会话可能已被删除或暂不可访问。"
icon={MessageSquare}
/>
</div> </div>
) )
} }
if (!user?.id) { if (!user?.id) {
return ( return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground"> <div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState
title="请先登录"
description="登录后可查看会话并发送消息。"
icon={MessageSquare}
/>
</div> </div>
) )
} }
@@ -79,8 +90,12 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
const isParticipant = session.participants.some((participant) => participant.id === userId) const isParticipant = session.participants.some((participant) => participant.id === userId)
if (!isParticipant) { if (!isParticipant) {
return ( return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground"> <div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState
title="无法查看会话"
description="仅会话参与方可查看并发送消息。"
icon={MessageSquare}
/>
</div> </div>
) )
} }
@@ -89,8 +104,8 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
return ( return (
<div className="container mx-auto max-w-3xl px-4 py-8 h-[calc(100vh-3.5rem)] flex flex-col"> <div className="container mx-auto max-w-3xl px-4 py-8 h-[calc(100vh-3.5rem)] flex flex-col">
<Card className="flex-1 flex flex-col overflow-hidden hover:shadow-card-hover"> <Card className="flex-1 flex flex-col overflow-hidden border-border/80 shadow-sm">
<div className="border-b px-4 py-3 flex items-center gap-3 bg-muted/30"> <div className="border-b border-border/60 px-4 py-3 flex items-center gap-3 bg-background">
<Link href="/chat" className="text-muted-foreground hover:text-foreground"> <Link href="/chat" className="text-muted-foreground hover:text-foreground">
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Link> </Link>
@@ -101,7 +116,10 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
<div> <div>
<span className="text-sm font-medium">{other.nickname}</span> <span className="text-sm font-medium">{other.nickname}</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Badge variant="outline" className="text-[10px] px-1.5 py-0"> <Badge
variant={session.type === "order" ? "info" : "neutral"}
className="text-[10px] px-1.5 py-0 font-normal"
>
{session.type === "order" ? "订单会话" : "咨询会话"} {session.type === "order" ? "订单会话" : "咨询会话"}
</Badge> </Badge>
</div> </div>
@@ -114,7 +132,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
if (msg.type === "system") { if (msg.type === "system") {
return ( return (
<div key={msg.id} className="text-center"> <div key={msg.id} className="text-center">
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded"> <span className="text-xs text-muted-foreground bg-muted/60 px-2 py-1 rounded-full">
{msg.content} {msg.content}
</span> </span>
</div> </div>
@@ -143,8 +161,10 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
) : ( ) : (
<p <p
className={cn( className={cn(
"inline-block rounded-lg px-3 py-2 text-sm", "inline-block rounded-xl border px-3 py-2 text-sm",
isMine ? "bg-primary text-primary-foreground" : "bg-muted", isMine
? "border-primary bg-primary text-primary-foreground"
: "border-border/60 bg-muted/60",
)} )}
> >
{msg.content} {msg.content}
@@ -163,7 +183,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
</div> </div>
</ScrollArea> </ScrollArea>
<div className="border-t p-4 bg-muted/30"> <div className="border-t border-border/60 p-4 bg-background">
<input <input
ref={imageInputRef} ref={imageInputRef}
type="file" type="file"
@@ -209,7 +229,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
placeholder="输入消息..." placeholder="输入消息..."
className="flex-1" className="flex-1 border-border/60"
/> />
<Button <Button
type="button" type="button"
+22 -18
View File
@@ -2,7 +2,7 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card" import { EmptyState } from "@/components/ui/empty-state"
import { listChatSessions } from "@/lib/api" import { listChatSessions } from "@/lib/api"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { MessageSquare } from "lucide-react" import { MessageSquare } from "lucide-react"
@@ -34,15 +34,19 @@ export default function ChatListPage() {
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6"> <div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
<h1 className="text-2xl font-bold"></h1> <h1 className="text-2xl font-bold"></h1>
<div className="flex flex-col gap-3"> {sessions.length > 0 ? (
<div className="overflow-hidden rounded-xl border border-border/80 bg-card shadow-sm">
{sessions.map((session) => { {sessions.map((session) => {
const other = const other =
session.participants.find((participant) => participant.id !== userId) ?? session.participants.find((participant) => participant.id !== userId) ??
session.participants[0] session.participants[0]
return ( return (
<Link key={session.id} href={`/chat/${session.id}`} className="block"> <Link
<Card className="p-0 hover:bg-muted/50 transition-colors"> key={session.id}
<CardContent className="flex items-center gap-3 p-4"> href={`/chat/${session.id}`}
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">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarImage src={other.avatar} /> <AvatarImage src={other.avatar} />
<AvatarFallback>{other.nickname[0]}</AvatarFallback> <AvatarFallback>{other.nickname[0]}</AvatarFallback>
@@ -50,7 +54,10 @@ export default function ChatListPage() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-medium">{other.nickname}</span> <span className="text-sm font-medium">{other.nickname}</span>
<Badge variant="outline" className="text-[10px] px-1.5 py-0"> <Badge
variant={session.type === "order" ? "info" : "neutral"}
className="text-[10px] px-1.5 py-0 font-normal"
>
{session.type === "order" ? "订单" : "咨询"} {session.type === "order" ? "订单" : "咨询"}
</Badge> </Badge>
</div> </div>
@@ -58,26 +65,23 @@ export default function ChatListPage() {
</div> </div>
<div className="flex flex-col items-end gap-1 shrink-0"> <div className="flex flex-col items-end gap-1 shrink-0">
{session.unreadCount > 0 && ( {session.unreadCount > 0 && (
<Badge className="h-4 min-w-4 px-1 flex items-center justify-center text-[10px]"> <Badge className="h-4 min-w-4 px-1 flex items-center justify-center rounded-full text-[10px]">
{session.unreadCount} {session.unreadCount}
</Badge> </Badge>
)} )}
</div> </div>
</CardContent> </div>
</Card>
</Link> </Link>
) )
})} })}
{sessions.length === 0 && (
<Card className="hover:shadow-card-hover">
<CardContent className="py-8 text-center text-sm text-muted-foreground">
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
</CardContent>
</Card>
)}
</div> </div>
) : (
<EmptyState
title="暂无消息"
description="订单沟通和咨询会话会显示在这里。"
icon={MessageSquare}
/>
)}
</div> </div>
) )
} }
+41 -27
View File
@@ -3,6 +3,8 @@
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { EmptyState } from "@/components/ui/empty-state"
import { StatusBadge } from "@/components/ui/status-badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { listChatSessions, listOrders } from "@/lib/api" import { listChatSessions, listOrders } from "@/lib/api"
import { statusLabels } from "@/lib/constants" import { statusLabels } from "@/lib/constants"
@@ -14,21 +16,22 @@ import {
} from "@/lib/domain/order-filters" } from "@/lib/domain/order-filters"
import { useMyShop } from "@/lib/hooks/use-my-shop" import { useMyShop } from "@/lib/hooks/use-my-shop"
import type { OrderStatus, UserRole } from "@/lib/types" import type { OrderStatus, UserRole } from "@/lib/types"
import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { Clock, MessageSquare, RefreshCw } from "lucide-react" import { ClipboardList, Clock, MessageSquare, RefreshCw } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
const statusColors: Record<OrderStatus, string> = { type OrderStatusBadgeVariant = "success" | "warning" | "info" | "neutral" | "destructive"
pending_payment: "bg-yellow-100 text-yellow-800",
pending_accept: "bg-blue-100 text-blue-800", const statusVariants: Record<OrderStatus, OrderStatusBadgeVariant> = {
in_progress: "bg-green-100 text-green-800", pending_payment: "warning",
pending_close: "bg-orange-100 text-orange-800", pending_accept: "info",
pending_review: "bg-purple-100 text-purple-800", in_progress: "success",
disputed: "bg-red-100 text-red-800", pending_close: "info",
completed: "bg-gray-100 text-gray-800", pending_review: "info",
cancelled: "bg-gray-100 text-gray-500", disputed: "destructive",
completed: "success",
cancelled: "neutral",
} }
type TabFilter = "all" | "active" | "completed" | "disputed" type TabFilter = "all" | "active" | "completed" | "disputed"
@@ -71,11 +74,19 @@ export default function OrderListPage() {
} = useMyShop(currentRole === "owner") } = useMyShop(currentRole === "owner")
if (currentRole === "owner" && shopLoading) { if (currentRole === "owner" && shopLoading) {
return <div className="text-sm text-muted-foreground">...</div> return (
<div className="container mx-auto max-w-3xl px-4 py-8">
<EmptyState title="加载中" description="正在读取店铺订单视角..." icon={RefreshCw} />
</div>
)
} }
if (currentRole === "owner" && shopError) { if (currentRole === "owner" && shopError) {
return <div className="text-sm text-muted-foreground">{shopError}</div> return (
<div className="container mx-auto max-w-3xl px-4 py-8">
<EmptyState title="无法读取订单" description={shopError} icon={ClipboardList} />
</div>
)
} }
return ( return (
@@ -179,14 +190,14 @@ function OrderListContent({
</div> </div>
{!orderRole ? ( {!orderRole ? (
<Card className="hover:shadow-card-hover"> <EmptyState
<CardContent className="py-8 text-center text-sm text-muted-foreground"> title="当前身份暂不支持订单视角"
description="切换到客户、打手或店主身份后可查看对应订单。"
</CardContent> icon={ClipboardList}
</Card> />
) : ( ) : (
<Tabs value={tab} onValueChange={(v) => setTab(v as TabFilter | "pending")}> <Tabs value={tab} onValueChange={(v) => setTab(v as TabFilter | "pending")}>
<TabsList> <TabsList variant="line">
{tabs.map((item) => ( {tabs.map((item) => (
<TabsTrigger key={item.value} value={item.value}> <TabsTrigger key={item.value} value={item.value}>
{item.label} {item.label}
@@ -196,20 +207,23 @@ function OrderListContent({
<TabsContent value={tab} className="mt-4 space-y-4"> <TabsContent value={tab} className="mt-4 space-y-4">
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<Card className="hover:shadow-card-hover"> <EmptyState
<CardContent className="py-8 text-center text-sm text-muted-foreground"> title="暂无订单"
description="当前筛选下还没有订单记录。"
</CardContent> icon={ClipboardList}
</Card> />
) : ( ) : (
filtered.map((order) => ( filtered.map((order) => (
<Card key={order.id} className="hover:shadow-card-hover"> <Card key={order.id} className="border-border/80 shadow-sm">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base">{order.service.title}</CardTitle> <CardTitle className="text-base">{order.service.title}</CardTitle>
<Badge className={cn("text-xs", statusColors[order.status])}> <StatusBadge
status={statusVariants[order.status]}
className="text-xs font-normal"
>
{statusLabels[order.status]} {statusLabels[order.status]}
</Badge> </StatusBadge>
</div> </div>
<p className="text-sm text-muted-foreground">{order.service.title}</p> <p className="text-sm text-muted-foreground">{order.service.title}</p>
</CardHeader> </CardHeader>
+7 -5
View File
@@ -108,11 +108,12 @@ export default function OrderActions({
) )
return ( return (
<div className="flex gap-2 flex-wrap"> <div className="flex flex-wrap items-center gap-2">
{status === "pending_payment" && isConsumer && ( {status === "pending_payment" && isConsumer && (
<> <>
<Button <Button
variant="outline" variant="outline"
className="border-border/60"
onClick={() => { onClick={() => {
if (!currentUserId) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
@@ -144,6 +145,7 @@ export default function OrderActions({
{isConsumer && ( {isConsumer && (
<Button <Button
variant="outline" variant="outline"
className="border-border/60"
onClick={() => { onClick={() => {
if (!currentUserId) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
@@ -180,7 +182,7 @@ export default function OrderActions({
)} )}
{(status === "in_progress" || status === "pending_close") && resolvedChatSessionId && ( {(status === "in_progress" || status === "pending_close") && resolvedChatSessionId && (
<Button asChild> <Button variant="outline" className="border-border/60" asChild>
<Link href={`/chat/${resolvedChatSessionId}`}> <Link href={`/chat/${resolvedChatSessionId}`}>
<MessageSquare className="mr-1 h-4 w-4" /> <MessageSquare className="mr-1 h-4 w-4" />
@@ -245,7 +247,7 @@ export default function OrderActions({
)} )}
{status === "completed" && ( {status === "completed" && (
<Button variant="outline" asChild> <Button variant="outline" className="border-border/60" asChild>
<Link href={`/order/new?serviceId=${serviceId}`}> <Link href={`/order/new?serviceId=${serviceId}`}>
<RefreshCw className="mr-1 h-4 w-4" /> <RefreshCw className="mr-1 h-4 w-4" />
@@ -254,7 +256,7 @@ export default function OrderActions({
)} )}
{status === "cancelled" && ( {status === "cancelled" && (
<Button variant="outline" asChild> <Button variant="outline" className="border-border/60" asChild>
<Link href={`/order/new?serviceId=${serviceId}`}> <Link href={`/order/new?serviceId=${serviceId}`}>
<RefreshCw className="mr-1 h-4 w-4" /> <RefreshCw className="mr-1 h-4 w-4" />
@@ -263,7 +265,7 @@ export default function OrderActions({
)} )}
{status === "disputed" && ( {status === "disputed" && (
<Button variant="outline" asChild> <Button variant="outline" className="border-border/60" asChild>
<Link href={`/dispute/${orderId}`}></Link> <Link href={`/dispute/${orderId}`}></Link>
</Button> </Button>
)} )}