feat: wire order and chat state flow
This commit is contained in:
@@ -8,14 +8,15 @@ import { Badge } from "@/components/ui/badge"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
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 { mockChatMessages, mockChatSessions } from "@/lib/mock"
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
import { useChatStore } from "@/store/chat"
|
||||||
|
|
||||||
export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
const session = mockChatSessions.find((s) => s.id === id)
|
const session = useChatStore((state) => state.sessions.find((item) => item.id === id))
|
||||||
const messages = mockChatMessages.filter((m) => m.sessionId === id)
|
const messages = useChatStore((state) => state.messages.filter((item) => item.sessionId === id))
|
||||||
|
const sendTextMessage = useChatStore((state) => state.sendTextMessage)
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
const { user } = useAuthStore()
|
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"
|
className="flex gap-2 max-w-2xl mx-auto"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault()
|
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("")
|
setInput("")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import { Lock, MessageSquare } from "lucide-react"
|
import { Lock, MessageSquare } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
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 { 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() {
|
export default function ChatListPage() {
|
||||||
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
|
const userId = useAuthStore((state) => state.user?.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||||
<h1 className="text-2xl font-bold mb-6">消息</h1>
|
<h1 className="text-2xl font-bold mb-6">消息</h1>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{mockChatSessions.map((session) => {
|
{sessions.map((session) => {
|
||||||
const other = session.participants[1]
|
const other =
|
||||||
|
session.participants.find((participant) => participant.id !== userId) ??
|
||||||
|
session.participants[0]
|
||||||
return (
|
return (
|
||||||
<Link key={session.id} href={`/chat/${session.id}`}>
|
<Link key={session.id} href={`/chat/${session.id}`}>
|
||||||
<Card className="hover:bg-accent/30 transition-colors">
|
<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">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||||
暂无消息
|
暂无消息
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
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> = {
|
const disputeStatusLabels: Record<string, string> = {
|
||||||
open: "已提交",
|
open: "已提交",
|
||||||
@@ -24,7 +25,8 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
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 existingDispute = mockDisputes.find((d) => d.orderId === id)
|
||||||
const [reason, setReason] = useState("")
|
const [reason, setReason] = useState("")
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
@@ -70,6 +72,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
updateOrderStatus(id, "disputed")
|
||||||
setSubmitted(true)
|
setSubmitted(true)
|
||||||
router.replace(`/dispute/${id}?submitted=1`)
|
router.replace(`/dispute/${id}?submitted=1`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
|
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { notFound } from "next/navigation"
|
import { use, useEffect } from "react"
|
||||||
import OrderActions from "@/components/order-actions"
|
import OrderActions from "@/components/order-actions"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import { mockChatSessions, mockOrders, mockReviews } from "@/lib/mock"
|
import { mockReviews } from "@/lib/mock"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
|
import { useChatStore } from "@/store/chat"
|
||||||
|
import { useOrderStore } from "@/store/orders"
|
||||||
|
|
||||||
const normalStatusSteps: OrderStatus[] = [
|
const normalStatusSteps: OrderStatus[] = [
|
||||||
"pending_payment",
|
"pending_payment",
|
||||||
@@ -28,13 +32,28 @@ const disputedStatusSteps: OrderStatus[] = [
|
|||||||
|
|
||||||
const cancelledStatusSteps: OrderStatus[] = ["pending_payment", "pending_accept", "cancelled"]
|
const cancelledStatusSteps: OrderStatus[] = ["pending_payment", "pending_accept", "cancelled"]
|
||||||
|
|
||||||
export default async function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params
|
const { id } = use(params)
|
||||||
const order = mockOrders.find((o) => o.id === id)
|
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
||||||
if (!order) notFound()
|
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 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 =
|
const statusSteps =
|
||||||
order.status === "disputed"
|
order.status === "disputed"
|
||||||
? disputedStatusSteps
|
? disputedStatusSteps
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
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 { 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) {
|
function showFeedback(message: string) {
|
||||||
if (typeof window === "undefined") return
|
if (typeof window === "undefined") return
|
||||||
@@ -23,6 +26,8 @@ export default function NewOrderPage() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { requireAuth } = useRequireAuth()
|
const { requireAuth } = useRequireAuth()
|
||||||
|
const createOrder = useOrderStore((state) => state.createOrder)
|
||||||
|
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
|
||||||
const serviceId = searchParams.get("serviceId")
|
const serviceId = searchParams.get("serviceId")
|
||||||
|
|
||||||
const service = mockServices.find((s) => s.id === serviceId)
|
const service = mockServices.find((s) => s.id === serviceId)
|
||||||
@@ -41,8 +46,6 @@ export default function NewOrderPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalPrice = service.price * quantity
|
const totalPrice = service.price * quantity
|
||||||
const redirectOrderId =
|
|
||||||
mockOrders.find((order) => order.service.id === service.id)?.id ?? mockOrders[0]?.id
|
|
||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
return (
|
return (
|
||||||
@@ -186,15 +189,28 @@ export default function NewOrderPage() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
requireAuth(async () => {
|
requireAuth(async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
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)
|
setSubmitted(true)
|
||||||
showFeedback("下单成功")
|
showFeedback("下单成功")
|
||||||
if (redirectOrderId) {
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
router.push(`/order/${order.id}`)
|
||||||
router.push(`/order/${redirectOrderId}`)
|
}, 800)
|
||||||
}, 800)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
router.push("/orders")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
+38
-18
@@ -8,10 +8,11 @@ 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import { mockChatSessions, mockOrders } from "@/lib/mock"
|
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
import { useChatStore } from "@/store/chat"
|
||||||
|
import { useOrderStore } from "@/store/orders"
|
||||||
|
|
||||||
const statusColors: Record<OrderStatus, string> = {
|
const statusColors: Record<OrderStatus, string> = {
|
||||||
pending_payment: "bg-yellow-100 text-yellow-800",
|
pending_payment: "bg-yellow-100 text-yellow-800",
|
||||||
@@ -51,6 +52,9 @@ const ownerTabs = [
|
|||||||
export default function OrderListPage() {
|
export default function OrderListPage() {
|
||||||
const [tab, setTab] = useState<TabFilter | "pending">("all")
|
const [tab, setTab] = useState<TabFilter | "pending">("all")
|
||||||
const { currentRole, user } = useAuthStore()
|
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 currentUserId = user?.id ?? "u1"
|
||||||
const ownerShopId = "shop1"
|
const ownerShopId = "shop1"
|
||||||
|
|
||||||
@@ -62,7 +66,14 @@ export default function OrderListPage() {
|
|||||||
const tabs =
|
const tabs =
|
||||||
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
|
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 === "consumer") return order.consumerId === currentUserId
|
||||||
if (currentRole === "player") return order.playerId === currentUserId
|
if (currentRole === "player") return order.playerId === currentUserId
|
||||||
return order.shopId === ownerShopId
|
return order.shopId === ownerShopId
|
||||||
@@ -70,8 +81,15 @@ export default function OrderListPage() {
|
|||||||
|
|
||||||
const filtered = roleFiltered.filter((order) => {
|
const filtered = roleFiltered.filter((order) => {
|
||||||
if (tab === "pending") return order.status === "pending_accept"
|
if (tab === "pending") return order.status === "pending_accept"
|
||||||
if (tab === "active")
|
if (tab === "active") {
|
||||||
return ["in_progress", "pending_close", "pending_review"].includes(order.status)
|
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 === "completed") return order.status === "completed" || order.status === "cancelled"
|
||||||
if (tab === "disputed") return order.status === "disputed"
|
if (tab === "disputed") return order.status === "disputed"
|
||||||
return true
|
return true
|
||||||
@@ -130,20 +148,22 @@ export default function OrderListPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{order.status === "in_progress" &&
|
{(() => {
|
||||||
(() => {
|
if (order.status !== "in_progress" && order.status !== "pending_close")
|
||||||
const session = mockChatSessions.find((s) => s.orderId === order.id)
|
return null
|
||||||
return (
|
const session = sessions.find(
|
||||||
session && (
|
(item) => item.type === "order" && item.orderId === order.id,
|
||||||
<Button variant="outline" size="sm" asChild>
|
)
|
||||||
<Link href={`/chat/${session.id}`}>
|
if (!session) return null
|
||||||
<MessageSquare className="mr-1 h-3.5 w-3.5" />
|
return (
|
||||||
聊天
|
<Button variant="outline" size="sm" asChild>
|
||||||
</Link>
|
<Link href={`/chat/${session.id}`}>
|
||||||
</Button>
|
<MessageSquare className="mr-1 h-3.5 w-3.5" />
|
||||||
)
|
聊天
|
||||||
)
|
</Link>
|
||||||
})()}
|
</Button>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
{order.status === "completed" && (
|
{order.status === "completed" && (
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link href={`/order/new?serviceId=${order.service.id}`}>
|
<Link href={`/order/new?serviceId=${order.service.id}`}>
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ 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 { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
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 }> }) {
|
export default function ReviewPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = use(params)
|
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 [rating, setRating] = useState(0)
|
||||||
const [hoverRating, setHoverRating] = useState(0)
|
const [hoverRating, setHoverRating] = useState(0)
|
||||||
const [content, setContent] = useState("")
|
const [content, setContent] = useState("")
|
||||||
@@ -97,7 +98,14 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
|
|||||||
<span>评价采用密封机制:你的评价将在双方都提交后同时揭晓。</span>
|
<span>评价采用密封机制:你的评价将在双方都提交后同时揭晓。</span>
|
||||||
</div>
|
</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>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
import { AlertTriangle, CheckCircle2, MessageSquare, RefreshCw, Star, XCircle } from "lucide-react"
|
import { AlertTriangle, CheckCircle2, MessageSquare, RefreshCw, Star, XCircle } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
|
import { useChatStore } from "@/store/chat"
|
||||||
|
import { useOrderStore } from "@/store/orders"
|
||||||
|
|
||||||
interface OrderActionsProps {
|
interface OrderActionsProps {
|
||||||
orderId: string
|
orderId: string
|
||||||
@@ -24,16 +26,32 @@ export default function OrderActions({
|
|||||||
chatSessionId,
|
chatSessionId,
|
||||||
serviceId,
|
serviceId,
|
||||||
}: OrderActionsProps) {
|
}: 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 (
|
return (
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{status === "pending_accept" && (
|
{status === "pending_payment" && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStatus("cancelled")
|
updateOrderStatus(orderId, "cancelled")
|
||||||
showFeedback("订单已取消")
|
showFeedback("订单已取消")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -42,7 +60,30 @@ export default function OrderActions({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
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("已接单")
|
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>
|
<Button asChild>
|
||||||
<Link href={`/chat/${chatSessionId}`}>
|
<Link href={`/chat/${resolvedChatSessionId}`}>
|
||||||
<MessageSquare className="mr-1 h-4 w-4" />
|
<MessageSquare className="mr-1 h-4 w-4" />
|
||||||
聊天
|
聊天
|
||||||
</Link>
|
</Link>
|
||||||
@@ -65,7 +106,7 @@ export default function OrderActions({
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStatus("pending_close")
|
updateOrderStatus(orderId, "pending_close")
|
||||||
showFeedback("已发起结单")
|
showFeedback("已发起结单")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -84,8 +125,7 @@ export default function OrderActions({
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStatus("completed")
|
updateOrderStatus(orderId, "pending_review")
|
||||||
showFeedback("订单已完成")
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认结单
|
确认结单
|
||||||
|
|||||||
+102
@@ -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,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user