feat: wire order and chat state flow

This commit is contained in:
zetaloop
2026-02-22 06:40:40 +08:00
parent 4ce7303258
commit 02269dd9c3
10 changed files with 372 additions and 57 deletions
+19 -3
View File
@@ -8,14 +8,15 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { mockChatMessages, mockChatSessions } from "@/lib/mock"
import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const session = mockChatSessions.find((s) => s.id === id)
const messages = mockChatMessages.filter((m) => m.sessionId === id)
const session = useChatStore((state) => state.sessions.find((item) => item.id === id))
const messages = useChatStore((state) => state.messages.filter((item) => item.sessionId === id))
const sendTextMessage = useChatStore((state) => state.sendTextMessage)
const [input, setInput] = useState("")
const { user } = useAuthStore()
@@ -103,6 +104,21 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
className="flex gap-2 max-w-2xl mx-auto"
onSubmit={(e) => {
e.preventDefault()
const text = input.trim()
if (!text) return
const sender = session.participants.find(
(participant) => participant.id === currentUserId,
)
sendTextMessage(
session.id,
{
id: currentUserId,
name: sender?.name ?? user?.nickname ?? "",
avatar: sender?.avatar ?? user?.avatar ?? "",
},
text,
)
setInput("")
}}
>
+12 -4
View File
@@ -1,18 +1,26 @@
"use client"
import { Lock, MessageSquare } from "lucide-react"
import Link from "next/link"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { mockChatSessions } from "@/lib/mock"
import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
export default function ChatListPage() {
const sessions = useChatStore((state) => state.sessions)
const userId = useAuthStore((state) => state.user?.id)
return (
<div className="container mx-auto py-8 px-4 max-w-2xl">
<h1 className="text-2xl font-bold mb-6"></h1>
<div className="space-y-2">
{mockChatSessions.map((session) => {
const other = session.participants[1]
{sessions.map((session) => {
const other =
session.participants.find((participant) => participant.id !== userId) ??
session.participants[0]
return (
<Link key={session.id} href={`/chat/${session.id}`}>
<Card className="hover:bg-accent/30 transition-colors">
@@ -49,7 +57,7 @@ export default function ChatListPage() {
)
})}
{mockChatSessions.length === 0 && (
{sessions.length === 0 && (
<div className="text-center py-12 text-muted-foreground">
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
+5 -2
View File
@@ -11,7 +11,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea"
import { mockDisputes, mockOrders } from "@/lib/mock"
import { mockDisputes } from "@/lib/mock"
import { useOrderStore } from "@/store/orders"
const disputeStatusLabels: Record<string, string> = {
open: "已提交",
@@ -24,7 +25,8 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
const { id } = use(params)
const router = useRouter()
const searchParams = useSearchParams()
const order = mockOrders.find((o) => o.id === id)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const updateOrderStatus = useOrderStore((state) => state.updateOrderStatus)
const existingDispute = mockDisputes.find((d) => d.orderId === id)
const [reason, setReason] = useState("")
const [submitted, setSubmitted] = useState(false)
@@ -70,6 +72,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
}
const handleSubmit = () => {
updateOrderStatus(id, "disputed")
setSubmitted(true)
router.replace(`/dispute/${id}?submitted=1`)
}
+26 -7
View File
@@ -1,13 +1,17 @@
"use client"
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
import Link from "next/link"
import { notFound } from "next/navigation"
import { use, useEffect } from "react"
import OrderActions from "@/components/order-actions"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
import { statusLabels } from "@/lib/constants"
import { mockChatSessions, mockOrders, mockReviews } from "@/lib/mock"
import { mockReviews } from "@/lib/mock"
import type { OrderStatus } from "@/lib/types"
import { useChatStore } from "@/store/chat"
import { useOrderStore } from "@/store/orders"
const normalStatusSteps: OrderStatus[] = [
"pending_payment",
@@ -28,13 +32,28 @@ const disputedStatusSteps: OrderStatus[] = [
const cancelledStatusSteps: OrderStatus[] = ["pending_payment", "pending_accept", "cancelled"]
export default async function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const order = mockOrders.find((o) => o.id === id)
if (!order) notFound()
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const sessions = useChatStore((state) => state.sessions)
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
useEffect(() => {
if (!order) return
if (order.status === "pending_payment" || order.status === "cancelled") return
ensureOrderSession(order)
}, [order, ensureOrderSession])
if (!order) {
return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
</div>
)
}
const reviews = mockReviews.filter((r) => r.orderId === id)
const chatSession = mockChatSessions.find((s) => s.orderId === id)
const chatSession = sessions.find((session) => session.type === "order" && session.orderId === id)
const statusSteps =
order.status === "disputed"
? disputedStatusSteps
+24 -8
View File
@@ -11,8 +11,11 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea"
import { mockOrders, mockPlayers, mockServices, walletBalance } from "@/lib/mock"
import { mockPlayers, mockServices, walletBalance } from "@/lib/mock"
import { useRequireAuth } from "@/lib/use-require-auth"
import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
import { useOrderStore } from "@/store/orders"
function showFeedback(message: string) {
if (typeof window === "undefined") return
@@ -23,6 +26,8 @@ export default function NewOrderPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { requireAuth } = useRequireAuth()
const createOrder = useOrderStore((state) => state.createOrder)
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
const serviceId = searchParams.get("serviceId")
const service = mockServices.find((s) => s.id === serviceId)
@@ -41,8 +46,6 @@ export default function NewOrderPage() {
}
const totalPrice = service.price * quantity
const redirectOrderId =
mockOrders.find((order) => order.service.id === service.id)?.id ?? mockOrders[0]?.id
if (submitted) {
return (
@@ -186,15 +189,28 @@ export default function NewOrderPage() {
onClick={() =>
requireAuth(async () => {
await new Promise((resolve) => setTimeout(resolve, 500))
const currentUser = useAuthStore.getState().user
if (!currentUser) return
const order = createOrder({
consumerId: currentUser.id,
consumerName: currentUser.nickname,
playerId: player.id,
playerName: player.user.nickname,
shopId: player.shopId,
shopName: player.shopName,
service,
totalPrice,
note,
status: "pending_accept",
})
ensureOrderSession(order)
setSubmitted(true)
showFeedback("下单成功")
if (redirectOrderId) {
setTimeout(() => {
router.push(`/order/${redirectOrderId}`)
router.push(`/order/${order.id}`)
}, 800)
return
}
router.push("/orders")
})
}
>
+29 -9
View File
@@ -8,10 +8,11 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { statusLabels } from "@/lib/constants"
import { mockChatSessions, mockOrders } from "@/lib/mock"
import type { OrderStatus } from "@/lib/types"
import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
import { useOrderStore } from "@/store/orders"
const statusColors: Record<OrderStatus, string> = {
pending_payment: "bg-yellow-100 text-yellow-800",
@@ -51,6 +52,9 @@ const ownerTabs = [
export default function OrderListPage() {
const [tab, setTab] = useState<TabFilter | "pending">("all")
const { currentRole, user } = useAuthStore()
const orders = useOrderStore((state) => state.orders)
const sessions = useChatStore((state) => state.sessions)
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
const currentUserId = user?.id ?? "u1"
const ownerShopId = "shop1"
@@ -62,7 +66,14 @@ export default function OrderListPage() {
const tabs =
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
const roleFiltered = mockOrders.filter((order) => {
useEffect(() => {
orders.forEach((order) => {
if (order.status === "pending_payment" || order.status === "cancelled") return
ensureOrderSession(order)
})
}, [orders, ensureOrderSession])
const roleFiltered = orders.filter((order) => {
if (currentRole === "consumer") return order.consumerId === currentUserId
if (currentRole === "player") return order.playerId === currentUserId
return order.shopId === ownerShopId
@@ -70,8 +81,15 @@ export default function OrderListPage() {
const filtered = roleFiltered.filter((order) => {
if (tab === "pending") return order.status === "pending_accept"
if (tab === "active")
return ["in_progress", "pending_close", "pending_review"].includes(order.status)
if (tab === "active") {
return [
"pending_payment",
"pending_accept",
"in_progress",
"pending_close",
"pending_review",
].includes(order.status)
}
if (tab === "completed") return order.status === "completed" || order.status === "cancelled"
if (tab === "disputed") return order.status === "disputed"
return true
@@ -130,11 +148,14 @@ export default function OrderListPage() {
</span>
</div>
<div className="flex gap-2">
{order.status === "in_progress" &&
(() => {
const session = mockChatSessions.find((s) => s.orderId === order.id)
{(() => {
if (order.status !== "in_progress" && order.status !== "pending_close")
return null
const session = sessions.find(
(item) => item.type === "order" && item.orderId === order.id,
)
if (!session) return null
return (
session && (
<Button variant="outline" size="sm" asChild>
<Link href={`/chat/${session.id}`}>
<MessageSquare className="mr-1 h-3.5 w-3.5" />
@@ -142,7 +163,6 @@ export default function OrderListPage() {
</Link>
</Button>
)
)
})()}
{order.status === "completed" && (
<Button variant="outline" size="sm" asChild>
+11 -3
View File
@@ -7,11 +7,12 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { mockOrders } from "@/lib/mock"
import { useOrderStore } from "@/store/orders"
export default function ReviewPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const order = mockOrders.find((o) => o.id === id)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const updateOrderStatus = useOrderStore((state) => state.updateOrderStatus)
const [rating, setRating] = useState(0)
const [hoverRating, setHoverRating] = useState(0)
const [content, setContent] = useState("")
@@ -97,7 +98,14 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
<span></span>
</div>
<Button className="w-full" disabled={rating === 0} onClick={() => setSubmitted(true)}>
<Button
className="w-full"
disabled={rating === 0}
onClick={() => {
updateOrderStatus(id, "completed")
setSubmitted(true)
}}
>
</Button>
</CardContent>
+50 -10
View File
@@ -2,9 +2,11 @@
import { AlertTriangle, CheckCircle2, MessageSquare, RefreshCw, Star, XCircle } from "lucide-react"
import Link from "next/link"
import { useState } from "react"
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import type { OrderStatus } from "@/lib/types"
import { useChatStore } from "@/store/chat"
import { useOrderStore } from "@/store/orders"
interface OrderActionsProps {
orderId: string
@@ -24,16 +26,32 @@ export default function OrderActions({
chatSessionId,
serviceId,
}: OrderActionsProps) {
const [status, setStatus] = useState<OrderStatus>(initialStatus)
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
const updateOrderStatus = useOrderStore((state) => state.updateOrderStatus)
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
const [resolvedChatSessionId, setResolvedChatSessionId] = useState(chatSessionId)
const status = order?.status ?? initialStatus
useEffect(() => {
if (chatSessionId) {
setResolvedChatSessionId(chatSessionId)
return
}
if (!order) return
const session = ensureOrderSession(order)
setResolvedChatSessionId(session.id)
}, [chatSessionId, order, ensureOrderSession])
return (
<div className="flex gap-2 flex-wrap">
{status === "pending_accept" && (
{status === "pending_payment" && (
<>
<Button
variant="outline"
onClick={() => {
setStatus("cancelled")
updateOrderStatus(orderId, "cancelled")
showFeedback("订单已取消")
}}
>
@@ -42,7 +60,30 @@ export default function OrderActions({
</Button>
<Button
onClick={() => {
setStatus("in_progress")
updateOrderStatus(orderId, "pending_accept")
}}
>
<CheckCircle2 className="mr-1 h-4 w-4" />
</Button>
</>
)}
{status === "pending_accept" && (
<>
<Button
variant="outline"
onClick={() => {
updateOrderStatus(orderId, "cancelled")
showFeedback("订单已取消")
}}
>
<XCircle className="mr-1 h-4 w-4" />
</Button>
<Button
onClick={() => {
updateOrderStatus(orderId, "in_progress")
showFeedback("已接单")
}}
>
@@ -52,9 +93,9 @@ export default function OrderActions({
</>
)}
{(status === "in_progress" || status === "pending_close") && chatSessionId && (
{(status === "in_progress" || status === "pending_close") && resolvedChatSessionId && (
<Button asChild>
<Link href={`/chat/${chatSessionId}`}>
<Link href={`/chat/${resolvedChatSessionId}`}>
<MessageSquare className="mr-1 h-4 w-4" />
</Link>
@@ -65,7 +106,7 @@ export default function OrderActions({
<>
<Button
onClick={() => {
setStatus("pending_close")
updateOrderStatus(orderId, "pending_close")
showFeedback("已发起结单")
}}
>
@@ -84,8 +125,7 @@ export default function OrderActions({
<>
<Button
onClick={() => {
setStatus("completed")
showFeedback("订单已完成")
updateOrderStatus(orderId, "pending_review")
}}
>
+102
View File
@@ -0,0 +1,102 @@
import { create } from "zustand"
import { mockChatMessages, mockChatSessions, mockUsers } from "@/lib/mock"
import type { ChatMessage, ChatSession, Order } from "@/lib/types"
interface Sender {
id: string
name: string
avatar: string
}
interface ChatState {
sessions: ChatSession[]
messages: ChatMessage[]
ensureOrderSession: (order: Order) => ChatSession
sendTextMessage: (sessionId: string, sender: Sender, content: string) => void
}
function resolveAvatar(userId: string) {
return mockUsers.find((user) => user.id === userId)?.avatar ?? ""
}
function shouldReadonly(status: Order["status"]) {
return status === "pending_review" || status === "completed" || status === "cancelled"
}
export const useChatStore = create<ChatState>((set, get) => ({
sessions: mockChatSessions,
messages: mockChatMessages,
ensureOrderSession: (order) => {
const existing = get().sessions.find(
(session) => session.type === "order" && session.orderId === order.id,
)
const readonly = shouldReadonly(order.status)
if (existing) {
if (existing.readonly !== readonly) {
set((state) => ({
sessions: state.sessions.map((session) =>
session.id === existing.id ? { ...session, readonly } : session,
),
}))
}
return get().sessions.find((session) => session.id === existing.id) ?? existing
}
const session: ChatSession = {
id: `chat-${order.id}`,
type: "order",
orderId: order.id,
participants: [
{
id: order.consumerId,
name: order.consumerName,
avatar: resolveAvatar(order.consumerId),
},
{
id: order.playerId,
name: order.playerName,
avatar: resolveAvatar(order.playerId),
},
],
unreadCount: 0,
readonly,
}
set((state) => ({
sessions: [session, ...state.sessions],
}))
return session
},
sendTextMessage: (sessionId, sender, content) => {
const text = content.trim()
if (!text) return
const now = new Date().toISOString()
const message: ChatMessage = {
id: `msg-${Date.now()}`,
sessionId,
senderId: sender.id,
senderName: sender.name,
senderAvatar: sender.avatar,
type: "text",
content: text,
createdAt: now,
}
set((state) => ({
messages: [...state.messages, message],
sessions: state.sessions.map((session) =>
session.id === sessionId
? {
...session,
lastMessage: text,
lastMessageAt: now,
}
: session,
),
}))
},
}))
+83
View File
@@ -0,0 +1,83 @@
import { create } from "zustand"
import { mockOrders } from "@/lib/mock"
import type { Order, OrderStatus, PlayerService } from "@/lib/types"
interface CreateOrderInput {
consumerId: string
consumerName: string
playerId: string
playerName: string
shopId?: string
shopName?: string
service: PlayerService
totalPrice: number
note?: string
status?: OrderStatus
}
interface OrderState {
orders: Order[]
createOrder: (input: CreateOrderInput) => Order
updateOrderStatus: (orderId: string, status: OrderStatus) => void
}
export const useOrderStore = create<OrderState>((set) => ({
orders: mockOrders,
createOrder: (input) => {
const order: Order = {
id: `ord${Date.now()}`,
consumerId: input.consumerId,
consumerName: input.consumerName,
playerId: input.playerId,
playerName: input.playerName,
shopId: input.shopId,
shopName: input.shopName,
service: input.service,
status: input.status ?? "pending_payment",
totalPrice: input.totalPrice,
note: input.note?.trim() ? input.note.trim() : undefined,
createdAt: new Date().toISOString(),
}
set((state) => ({
orders: [order, ...state.orders],
}))
return order
},
updateOrderStatus: (orderId, status) =>
set((state) => ({
orders: state.orders.map((order) => {
if (order.id !== orderId) return order
const now = new Date().toISOString()
switch (status) {
case "in_progress":
return {
...order,
status,
acceptedAt: order.acceptedAt ?? now,
}
case "pending_review":
return {
...order,
status,
closedAt: order.closedAt ?? now,
}
case "completed":
return {
...order,
status,
closedAt: order.closedAt ?? now,
completedAt: order.completedAt ?? now,
}
default:
return {
...order,
status,
}
}
}),
})),
}))