feat(orders): migrate orders to backend API
This commit is contained in:
@@ -19,7 +19,8 @@ export default function DashboardPage() {
|
|||||||
const [player, setPlayer] = useState<Player | null>(null)
|
const [player, setPlayer] = useState<Player | null>(null)
|
||||||
const [shop, setShop] = useState<Shop | null>(null)
|
const [shop, setShop] = useState<Shop | null>(null)
|
||||||
const [services, setServices] = useState<PlayerService[]>([])
|
const [services, setServices] = useState<PlayerService[]>([])
|
||||||
const recentOrders = listOrders().slice(0, 3)
|
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
|
||||||
|
const recentOrders = orders.slice(0, 3)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -43,6 +44,25 @@ export default function DashboardPage() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const orders = await Promise.resolve(listOrders())
|
||||||
|
if (cancelled) return
|
||||||
|
setOrders(orders)
|
||||||
|
} catch {
|
||||||
|
if (cancelled) return
|
||||||
|
setOrders([])
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const totalOrders = isOwner ? (shop?.totalOrders ?? 0) : (player?.totalOrders ?? 0)
|
const totalOrders = isOwner ? (shop?.totalOrders ?? 0) : (player?.totalOrders ?? 0)
|
||||||
const rating = isOwner ? (shop?.rating ?? 0) : (player?.rating ?? 0)
|
const rating = isOwner ? (shop?.rating ?? 0) : (player?.rating ?? 0)
|
||||||
const playerCount = shop?.playerCount ?? 0
|
const playerCount = shop?.playerCount ?? 0
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import { useEffect, useState } from "react"
|
|||||||
export default function CommunityPage() {
|
export default function CommunityPage() {
|
||||||
const [games, setGames] = useState<Game[]>([])
|
const [games, setGames] = useState<Game[]>([])
|
||||||
const [players, setPlayers] = useState<Player[]>([])
|
const [players, setPlayers] = useState<Player[]>([])
|
||||||
|
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
|
||||||
const [posts, setPosts] = useState<Post[]>([])
|
const [posts, setPosts] = useState<Post[]>([])
|
||||||
const [postsLoading, setPostsLoading] = useState(true)
|
const [postsLoading, setPostsLoading] = useState(true)
|
||||||
const orders = listOrders()
|
|
||||||
|
|
||||||
const [sortMode, setSortMode] = useState<"latest" | "hot">("latest")
|
const [sortMode, setSortMode] = useState<"latest" | "hot">("latest")
|
||||||
const [selectedGame, setSelectedGame] = useState<string | null>(null)
|
const [selectedGame, setSelectedGame] = useState<string | null>(null)
|
||||||
@@ -26,11 +26,12 @@ export default function CommunityPage() {
|
|||||||
|
|
||||||
setPostsLoading(true)
|
setPostsLoading(true)
|
||||||
|
|
||||||
Promise.all([listGames(), listPlayers(), listPosts()])
|
Promise.all([listGames(), listPlayers(), Promise.resolve(listOrders()), listPosts()])
|
||||||
.then(([gamesItems, playersItems, postsItems]) => {
|
.then(([gamesItems, playersItems, ordersItems, postsItems]) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setGames(gamesItems)
|
setGames(gamesItems)
|
||||||
setPlayers(playersItems)
|
setPlayers(playersItems)
|
||||||
|
setOrders(ordersItems)
|
||||||
setPosts(postsItems)
|
setPosts(postsItems)
|
||||||
setPostsLoading(false)
|
setPostsLoading(false)
|
||||||
})
|
})
|
||||||
@@ -38,6 +39,7 @@ export default function CommunityPage() {
|
|||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setGames([])
|
setGames([])
|
||||||
setPlayers([])
|
setPlayers([])
|
||||||
|
setOrders([])
|
||||||
setPosts([])
|
setPosts([])
|
||||||
setPostsLoading(false)
|
setPostsLoading(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { getOrderById, getPlayerById, getPostById } from "@/lib/api"
|
import { getPostById } from "@/lib/api"
|
||||||
import { roleLabels } from "@/lib/constants"
|
import { roleLabels } from "@/lib/constants"
|
||||||
import { ArrowLeft, Pin, Star } from "lucide-react"
|
import { ArrowLeft, Pin, Star } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
@@ -17,9 +17,6 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
|
|||||||
const post = await getPostById(id)
|
const post = await getPostById(id)
|
||||||
if (!post) notFound()
|
if (!post) notFound()
|
||||||
|
|
||||||
const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : null
|
|
||||||
const linkedPlayer = linkedOrder ? await getPlayerById(linkedOrder.playerId) : null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
<Link
|
<Link
|
||||||
@@ -65,17 +62,14 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{linkedOrder && (
|
{post.linkedOrderId && (
|
||||||
<Link href={`/order/${linkedOrder.id}`}>
|
<Link href={`/order/${post.linkedOrderId}`}>
|
||||||
<div className="rounded-lg border bg-muted/30 p-3 text-sm hover:bg-muted/50 transition-colors">
|
<div className="rounded-lg border bg-muted/30 p-3 text-sm hover:bg-muted/50 transition-colors">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Star className="h-3.5 w-3.5 text-yellow-500" />
|
<Star className="h-3.5 w-3.5 text-yellow-500" />
|
||||||
<span className="font-medium">关联订单</span>
|
<span className="font-medium">关联订单</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">点击查看订单详情</p>
|
||||||
{linkedOrder.service.gameName} · {linkedOrder.service.title} · 评分{" "}
|
|
||||||
{linkedPlayer?.rating ?? "--"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ 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 { getOrderById } from "@/lib/api"
|
||||||
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
|
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { OrderStatus } from "@/lib/types"
|
||||||
import { useChatStore } from "@/store/chat"
|
import { useChatStore } from "@/store/chat"
|
||||||
import { useOrderStore } from "@/store/orders"
|
|
||||||
import { useReviewStore } from "@/store/reviews"
|
import { useReviewStore } from "@/store/reviews"
|
||||||
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"
|
||||||
@@ -35,7 +35,6 @@ const cancelledStatusSteps: OrderStatus[] = ["pending_payment", "pending_accept"
|
|||||||
|
|
||||||
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
|
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
const allReviews = useReviewStore((state) => state.reviews)
|
const allReviews = useReviewStore((state) => state.reviews)
|
||||||
// Filtering is deferred to useMemo after reading the raw store array.
|
// Filtering is deferred to useMemo after reading the raw store array.
|
||||||
@@ -43,8 +42,34 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
// Returning a fresh filtered array from the selector can re-trigger updates
|
// Returning a fresh filtered array from the selector can re-trigger updates
|
||||||
// and loop under useSyncExternalStore (pmndrs/zustand#1936, #3155).
|
// and loop under useSyncExternalStore (pmndrs/zustand#1936, #3155).
|
||||||
const reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id])
|
const reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id])
|
||||||
|
const [order, setOrder] = useState<Awaited<ReturnType<typeof getOrderById>> | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
const [nowTs, setNowTs] = useState(0)
|
const [nowTs, setNowTs] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const order = await Promise.resolve(getOrderById(id))
|
||||||
|
if (cancelled) return
|
||||||
|
setOrder(order)
|
||||||
|
setLoading(false)
|
||||||
|
} catch {
|
||||||
|
if (cancelled) return
|
||||||
|
setOrder(undefined)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!order) return
|
if (!order) return
|
||||||
if (order.status !== "pending_accept" && order.status !== "pending_close") return
|
if (order.status !== "pending_accept" && order.status !== "pending_close") return
|
||||||
@@ -56,6 +81,12 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [order])
|
}, [order])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">加载中...</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||||||
@@ -257,6 +288,8 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
|
|||||||
|
|
||||||
<OrderActions
|
<OrderActions
|
||||||
orderId={order.id}
|
orderId={order.id}
|
||||||
|
order={order}
|
||||||
|
onOrderChange={(next) => setOrder(next)}
|
||||||
initialStatus={order.status}
|
initialStatus={order.status}
|
||||||
chatSessionId={chatSession?.id}
|
chatSessionId={chatSession?.id}
|
||||||
serviceId={order.service.id}
|
serviceId={order.service.id}
|
||||||
|
|||||||
@@ -7,13 +7,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 type { Actor } from "@/lib/actor"
|
|
||||||
import { getPlayerById, getServiceById } from "@/lib/api"
|
import { getPlayerById, getServiceById } from "@/lib/api"
|
||||||
|
import { createPaidOrder } from "@/lib/api/orders"
|
||||||
import { notifySuccess } from "@/lib/toast"
|
import { notifySuccess } from "@/lib/toast"
|
||||||
import type { Player, PlayerService } from "@/lib/types"
|
import type { Player, PlayerService } from "@/lib/types"
|
||||||
import { useRequireAuth } from "@/lib/use-require-auth"
|
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||||
import { useAuthStore } from "@/store/auth"
|
|
||||||
import { useOrderStore } from "@/store/orders"
|
|
||||||
import { useWalletStore } from "@/store/wallet"
|
import { useWalletStore } from "@/store/wallet"
|
||||||
import { ArrowLeft, CheckCircle, CreditCard, ShieldCheck } from "lucide-react"
|
import { ArrowLeft, CheckCircle, CreditCard, ShieldCheck } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
@@ -24,7 +22,6 @@ export default function NewOrderPage() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { requireAuth } = useRequireAuth()
|
const { requireAuth } = useRequireAuth()
|
||||||
const createPaidOrder = useOrderStore((state) => state.createPaidOrder)
|
|
||||||
const balance = useWalletStore((state) => state.balance)
|
const balance = useWalletStore((state) => state.balance)
|
||||||
const serviceId = searchParams.get("serviceId")
|
const serviceId = searchParams.get("serviceId")
|
||||||
|
|
||||||
@@ -234,24 +231,15 @@ export default function NewOrderPage() {
|
|||||||
disabled={balance < totalPrice}
|
disabled={balance < totalPrice}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
requireAuth(() => {
|
requireAuth(() => {
|
||||||
const authUser = useAuthStore.getState().user
|
Promise.resolve(
|
||||||
if (!authUser) return
|
createPaidOrder({
|
||||||
|
|
||||||
const actor: Actor = {
|
|
||||||
userId: authUser.id,
|
|
||||||
role: "consumer",
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = createPaidOrder(
|
|
||||||
{
|
|
||||||
playerId: player.id,
|
playerId: player.id,
|
||||||
serviceId: service.id,
|
serviceId: service.id,
|
||||||
shopId: player.shopId,
|
shopId: player.shopId,
|
||||||
quantity,
|
quantity,
|
||||||
note,
|
note,
|
||||||
},
|
}),
|
||||||
actor,
|
).then((result) => {
|
||||||
)
|
|
||||||
if (!result.decision.ok || !result.order) {
|
if (!result.decision.ok || !result.order) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -263,6 +251,7 @@ export default function NewOrderPage() {
|
|||||||
router.push(`/order/${nextOrder.id}`)
|
router.push(`/order/${nextOrder.id}`)
|
||||||
}, 800)
|
}, 800)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
确认支付 ¥{totalPrice}
|
确认支付 ¥{totalPrice}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { listOrders } from "@/lib/api"
|
||||||
import { statusLabels } from "@/lib/constants"
|
import { statusLabels } from "@/lib/constants"
|
||||||
import {
|
import {
|
||||||
isActiveOrder,
|
isActiveOrder,
|
||||||
@@ -16,11 +17,10 @@ import type { OrderStatus, UserRole } 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 { useChatStore } from "@/store/chat"
|
||||||
import { useOrderStore } from "@/store/orders"
|
|
||||||
import { useShopStore } from "@/store/shops"
|
import { useShopStore } from "@/store/shops"
|
||||||
import { Clock, MessageSquare, RefreshCw } from "lucide-react"
|
import { Clock, MessageSquare, RefreshCw } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
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",
|
||||||
@@ -82,9 +82,28 @@ function OrderListContent({
|
|||||||
ownerShopId?: string
|
ownerShopId?: string
|
||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<TabFilter | "pending">("all")
|
const [tab, setTab] = useState<TabFilter | "pending">("all")
|
||||||
const orders = useOrderStore((state) => state.orders)
|
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const items = await Promise.resolve(listOrders())
|
||||||
|
if (cancelled) return
|
||||||
|
setOrders(items)
|
||||||
|
} catch {
|
||||||
|
if (cancelled) return
|
||||||
|
setOrders([])
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const tabs =
|
const tabs =
|
||||||
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
|
currentRole === "consumer" ? consumerTabs : currentRole === "player" ? playerTabs : ownerTabs
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@/lib/api/orders"
|
} from "@/lib/api/orders"
|
||||||
import type { ApiDecision } from "@/lib/errors"
|
import type { ApiDecision } from "@/lib/errors"
|
||||||
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
import { notifyInfo, notifySuccess } from "@/lib/toast"
|
||||||
import type { OrderStatus } from "@/lib/types"
|
import type { Order, OrderStatus } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useChatStore } from "@/store/chat"
|
import { useChatStore } from "@/store/chat"
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
@@ -29,6 +29,8 @@ import { useCallback } from "react"
|
|||||||
|
|
||||||
interface OrderActionsProps {
|
interface OrderActionsProps {
|
||||||
orderId: string
|
orderId: string
|
||||||
|
order?: Order
|
||||||
|
onOrderChange?: (order: Order) => void
|
||||||
initialStatus: OrderStatus
|
initialStatus: OrderStatus
|
||||||
chatSessionId?: string
|
chatSessionId?: string
|
||||||
serviceId: string
|
serviceId: string
|
||||||
@@ -40,12 +42,15 @@ function showFeedback(message: string) {
|
|||||||
|
|
||||||
export default function OrderActions({
|
export default function OrderActions({
|
||||||
orderId,
|
orderId,
|
||||||
|
order: orderProp,
|
||||||
|
onOrderChange,
|
||||||
initialStatus,
|
initialStatus,
|
||||||
chatSessionId,
|
chatSessionId,
|
||||||
serviceId,
|
serviceId,
|
||||||
}: OrderActionsProps) {
|
}: OrderActionsProps) {
|
||||||
const currentUserId = useAuthStore((state) => state.user?.id)
|
const currentUserId = useAuthStore((state) => state.user?.id)
|
||||||
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
const storeOrder = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
|
||||||
|
const order = orderProp ?? storeOrder
|
||||||
const sessions = useChatStore((state) => state.sessions)
|
const sessions = useChatStore((state) => state.sessions)
|
||||||
const dispatchMode = useShopStore((state) => {
|
const dispatchMode = useShopStore((state) => {
|
||||||
if (!order?.shopId) return "manual"
|
if (!order?.shopId) return "manual"
|
||||||
@@ -60,7 +65,9 @@ export default function OrderActions({
|
|||||||
const isConsumer = order?.consumerId === currentUserId
|
const isConsumer = order?.consumerId === currentUserId
|
||||||
const isPlayer = order?.playerId === currentUserId
|
const isPlayer = order?.playerId === currentUserId
|
||||||
|
|
||||||
const handleDecision = useCallback((okMessage: string, result: { decision: ApiDecision }) => {
|
type ActionResult = { decision: ApiDecision; order?: Order }
|
||||||
|
|
||||||
|
const handleDecision = useCallback((okMessage: string, result: ActionResult) => {
|
||||||
if (result.decision.ok) {
|
if (result.decision.ok) {
|
||||||
showFeedback(okMessage)
|
showFeedback(okMessage)
|
||||||
return
|
return
|
||||||
@@ -69,6 +76,20 @@ export default function OrderActions({
|
|||||||
notifyInfo(result.decision.error.msg)
|
notifyInfo(result.decision.error.msg)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const runAction = useCallback(
|
||||||
|
(okMessage: string, actionCall: ActionResult | Promise<ActionResult>) => {
|
||||||
|
Promise.resolve(actionCall)
|
||||||
|
.then((result) => {
|
||||||
|
handleDecision(okMessage, result)
|
||||||
|
if (result.order) onOrderChange?.(result.order)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifyInfo("操作失败")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[handleDecision, onOrderChange],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{status === "pending_payment" && isConsumer && (
|
{status === "pending_payment" && isConsumer && (
|
||||||
@@ -80,8 +101,7 @@ export default function OrderActions({
|
|||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = cancelPreAccept(orderId)
|
runAction("订单已取消", cancelPreAccept(orderId))
|
||||||
handleDecision("订单已取消", result)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XCircle className="mr-1 h-4 w-4" />
|
<XCircle className="mr-1 h-4 w-4" />
|
||||||
@@ -93,8 +113,7 @@ export default function OrderActions({
|
|||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = payOrder(orderId)
|
runAction("订单支付成功", payOrder(orderId))
|
||||||
handleDecision("订单支付成功", result)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="mr-1 h-4 w-4" />
|
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||||
@@ -113,8 +132,7 @@ export default function OrderActions({
|
|||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = cancelPreAccept(orderId)
|
runAction("订单已取消", cancelPreAccept(orderId))
|
||||||
handleDecision("订单已取消", result)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XCircle className="mr-1 h-4 w-4" />
|
<XCircle className="mr-1 h-4 w-4" />
|
||||||
@@ -134,8 +152,7 @@ export default function OrderActions({
|
|||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = acceptOrder(orderId)
|
runAction("已接单", acceptOrder(orderId))
|
||||||
handleDecision("已接单", result)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="mr-1 h-4 w-4" />
|
<CheckCircle2 className="mr-1 h-4 w-4" />
|
||||||
@@ -163,8 +180,7 @@ export default function OrderActions({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = requestClose(orderId)
|
runAction("已发起结单", requestClose(orderId))
|
||||||
handleDecision("已发起结单", result)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
发起结单
|
发起结单
|
||||||
@@ -187,8 +203,7 @@ export default function OrderActions({
|
|||||||
notifyInfo("请先登录")
|
notifyInfo("请先登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const result = confirmClose(orderId)
|
runAction("已确认结单", confirmClose(orderId))
|
||||||
handleDecision("已确认结单", result)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认结单
|
确认结单
|
||||||
|
|||||||
+118
-59
@@ -1,21 +1,87 @@
|
|||||||
import type { Actor } from "@/lib/actor"
|
|
||||||
import { allow, deny } from "@/lib/decision"
|
import { allow, deny } from "@/lib/decision"
|
||||||
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
|
import { isApiError, toApiError, type ApiDecision } from "@/lib/errors"
|
||||||
import type { ApiDecision } from "@/lib/errors"
|
import type { Order, OrderStatus } from "@/lib/types"
|
||||||
import { useAuthStore } from "@/store/auth"
|
|
||||||
import { useOrderStore } from "@/store/orders"
|
|
||||||
import { useShopStore } from "@/store/shops"
|
|
||||||
|
|
||||||
export function listOrders() {
|
import { httpJson } from "./http"
|
||||||
return useOrderStore.getState().orders
|
|
||||||
|
type Paginated<T> = {
|
||||||
|
items: T[]
|
||||||
|
meta: {
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrderById(orderId: string) {
|
export type ListOrdersOptions = {
|
||||||
return useOrderStore.getState().orders.find((order) => order.id === orderId)
|
role?: "consumer" | "player" | "owner"
|
||||||
|
status?: OrderStatus
|
||||||
|
offset?: number
|
||||||
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listOrdersByConsumer(consumerId: string) {
|
type ActionResult = { decision: ApiDecision; order?: Order }
|
||||||
return useOrderStore.getState().orders.filter((order) => order.consumerId === consumerId)
|
|
||||||
|
function withOffsetLimit(path: string, options?: ListOrdersOptions): string {
|
||||||
|
const offset = options?.offset ?? 0
|
||||||
|
const limit = options?.limit ?? 1000
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
offset: String(offset),
|
||||||
|
limit: String(limit),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (options?.role) searchParams.set("role", options.role)
|
||||||
|
if (options?.status) searchParams.set("status", options.status)
|
||||||
|
|
||||||
|
return `${path}?${searchParams.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapOrder(value: unknown): Order | undefined {
|
||||||
|
if (typeof value !== "object" || value === null) return undefined
|
||||||
|
if ("order" in value) {
|
||||||
|
const envelope = value as { order?: Order }
|
||||||
|
return envelope.order
|
||||||
|
}
|
||||||
|
return value as Order
|
||||||
|
}
|
||||||
|
|
||||||
|
function denyFromError(error: unknown): ApiDecision {
|
||||||
|
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||||
|
return deny(401, "请先登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiError = toApiError(error)
|
||||||
|
return deny(apiError.code, apiError.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listOrders(options?: ListOrdersOptions): Promise<Order[]> {
|
||||||
|
const res = await httpJson<Paginated<Order>>(withOffsetLimit("/api/v1/orders", options), {
|
||||||
|
cache: "no-store",
|
||||||
|
})
|
||||||
|
return res.items
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrderById(orderId: string): Promise<Order | undefined> {
|
||||||
|
try {
|
||||||
|
const res = await httpJson<unknown>(`/api/v1/orders/${encodeURIComponent(orderId)}`, {
|
||||||
|
cache: "no-store",
|
||||||
|
})
|
||||||
|
return unwrapOrder(res)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
if (isApiError(error) && error.code === 404) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listOrdersByConsumer(consumerId: string): Promise<Order[]> {
|
||||||
|
const items = await listOrders({ role: "consumer" })
|
||||||
|
return items.filter((order) => order.consumerId === consumerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreatePaidOrderInput {
|
interface CreatePaidOrderInput {
|
||||||
@@ -26,69 +92,62 @@ interface CreatePaidOrderInput {
|
|||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveActorContext(): { actor?: Actor; decision: ApiDecision } {
|
export async function createPaidOrder(input: CreatePaidOrderInput): Promise<ActionResult> {
|
||||||
const auth = useAuthStore.getState()
|
try {
|
||||||
if (!auth.user?.id) {
|
const res = await httpJson<unknown>("/api/v1/orders/paid", {
|
||||||
return { decision: deny(401, "请先登录") }
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
json: input,
|
||||||
|
})
|
||||||
|
const order = unwrapOrder(res)
|
||||||
|
if (!order) {
|
||||||
|
return { decision: deny(500, "订单创建失败") }
|
||||||
}
|
}
|
||||||
|
return { decision: allow(), order }
|
||||||
const shopId =
|
} catch (error) {
|
||||||
auth.currentRole === "owner"
|
return { decision: denyFromError(error) }
|
||||||
? resolveOwnerShop(auth.user.id, useShopStore.getState().shops)?.id
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
actor: {
|
|
||||||
userId: auth.user.id,
|
|
||||||
role: auth.currentRole,
|
|
||||||
shopId,
|
|
||||||
},
|
|
||||||
decision: allow(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPaidOrder(input: CreatePaidOrderInput) {
|
async function postOrderAction(orderId: string, action: string): Promise<ActionResult> {
|
||||||
const { actor, decision } = resolveActorContext()
|
try {
|
||||||
if (!actor) return { decision }
|
const res = await httpJson<unknown>(`/api/v1/orders/${encodeURIComponent(orderId)}/${action}`, {
|
||||||
return useOrderStore.getState().createPaidOrder(input, actor)
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
})
|
||||||
|
|
||||||
|
const order = unwrapOrder(res)
|
||||||
|
if (order) {
|
||||||
|
return { decision: allow(), order }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function payOrder(orderId: string) {
|
const refetched = await getOrderById(orderId).catch(() => undefined)
|
||||||
const { actor, decision } = resolveActorContext()
|
if (refetched) {
|
||||||
if (!actor) return { decision }
|
return { decision: allow(), order: refetched }
|
||||||
return useOrderStore.getState().payOrder(orderId, actor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function acceptOrder(orderId: string) {
|
return { decision: allow() }
|
||||||
const { actor, decision } = resolveActorContext()
|
} catch (error) {
|
||||||
if (!actor) return { decision }
|
return { decision: denyFromError(error) }
|
||||||
return useOrderStore.getState().acceptOrder(orderId, actor)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function acceptOrderAsActor(orderId: string, actor: Actor) {
|
export async function payOrder(orderId: string): Promise<ActionResult> {
|
||||||
return useOrderStore.getState().acceptOrder(orderId, actor)
|
return postOrderAction(orderId, "pay")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestClose(orderId: string) {
|
export async function acceptOrder(orderId: string): Promise<ActionResult> {
|
||||||
const { actor, decision } = resolveActorContext()
|
return postOrderAction(orderId, "accept")
|
||||||
if (!actor) return { decision }
|
|
||||||
return useOrderStore.getState().requestClose(orderId, actor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function confirmClose(orderId: string) {
|
export async function requestClose(orderId: string): Promise<ActionResult> {
|
||||||
const { actor, decision } = resolveActorContext()
|
return postOrderAction(orderId, "request-close")
|
||||||
if (!actor) return { decision }
|
|
||||||
return useOrderStore.getState().confirmClose(orderId, actor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancelPreAccept(orderId: string) {
|
export async function confirmClose(orderId: string): Promise<ActionResult> {
|
||||||
const { actor, decision } = resolveActorContext()
|
return postOrderAction(orderId, "confirm-close")
|
||||||
if (!actor) return { decision }
|
|
||||||
return useOrderStore.getState().cancelPreAccept(orderId, actor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markDisputed(orderId: string) {
|
export async function cancelPreAccept(orderId: string): Promise<ActionResult> {
|
||||||
const { actor, decision } = resolveActorContext()
|
return postOrderAction(orderId, "cancel")
|
||||||
if (!actor) return { decision }
|
|
||||||
return useOrderStore.getState().markDisputed(orderId, actor)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { useAuthStore } from "@/store/auth"
|
|||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
describe("lib/api/orders", () => {
|
describe("lib/api/orders", () => {
|
||||||
it("returns { decision: { ok:false, error:{code,msg} } } when unauthenticated", () => {
|
it("returns { decision: { ok:false, error:{code,msg} } } when unauthenticated", async () => {
|
||||||
useAuthStore.getState().logout()
|
useAuthStore.getState().logout()
|
||||||
|
|
||||||
const res = createPaidOrder({
|
const res = await Promise.resolve(
|
||||||
|
createPaidOrder({
|
||||||
playerId: "1005",
|
playerId: "1005",
|
||||||
serviceId: "5001",
|
serviceId: "5001",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
decision: {
|
decision: {
|
||||||
|
|||||||
Reference in New Issue
Block a user