Files
juwan-frontend/app/(order)/order/new/page.tsx
T
2026-04-26 01:53:15 +08:00

292 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { EmptyState } from "@/components/ui/empty-state"
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 { getPlayerById, getServiceById, requestWithAuth } from "@/lib/api"
import { createPaidOrder } from "@/lib/api/orders"
import { getWalletBalance } from "@/lib/api/transactions"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { Player, PlayerService } from "@/lib/types"
import { useRequireAuth } from "@/lib/use-require-auth"
import { ArrowLeft, CheckCircle, CreditCard, ShieldCheck } from "lucide-react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
export default function NewOrderPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { requireAuth } = useRequireAuth()
const serviceId = searchParams.get("serviceId")
const [service, setService] = useState<PlayerService | null>(null)
const [player, setPlayer] = useState<Player | null>(null)
const [balance, setBalance] = useState<number | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
const load = async () => {
setLoading(true)
if (!serviceId) {
setService(null)
setPlayer(null)
setLoading(false)
return
}
try {
const s = await getServiceById(serviceId)
if (cancelled) return
if (!s) {
setService(null)
setPlayer(null)
setLoading(false)
return
}
setService(s)
const p = await getPlayerById(s.playerId)
if (cancelled) return
setPlayer(p ?? null)
setLoading(false)
} catch {
if (cancelled) return
setService(null)
setPlayer(null)
setLoading(false)
}
}
void load()
return () => {
cancelled = true
}
}, [serviceId])
useEffect(() => {
let cancelled = false
void requestWithAuth(() => getWalletBalance())
.then((nextBalance) => {
if (!cancelled) setBalance(nextBalance ?? null)
})
.catch(() => {
if (!cancelled) setBalance(null)
})
return () => {
cancelled = true
}
}, [])
const [quantity, setQuantity] = useState(1)
const [note, setNote] = useState("")
const [submitted, setSubmitted] = useState(false)
if (loading) {
return (
<div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState title="加载中" description="正在读取服务信息..." icon={CreditCard} />
</div>
)
}
if (!service || !player) {
return (
<div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState title="服务不存在" description="该服务可能已下架或暂不可访问。" />
</div>
)
}
const totalPrice = service.price * quantity
if (submitted) {
return (
<div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState
title="下单成功"
description="订单已创建,等待打手接单。你可以在订单列表中查看进度。"
icon={CheckCircle}
className="[&>div>svg]:text-success"
action={
<div className="flex gap-2 justify-center">
<Button asChild>
<Link href="/orders"></Link>
</Button>
<Button variant="outline" asChild>
<Link href={`/player/${player.id}`}></Link>
</Button>
</div>
}
/>
</div>
)
}
return (
<div className="container mx-auto py-8 px-4 max-w-2xl">
<Link
href={`/player/${player.id}`}
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
>
<ArrowLeft className="h-4 w-4" />
</Link>
<h1 className="text-2xl font-bold mb-6"></h1>
<div className="space-y-6">
<Card className="border-border/80 shadow-sm">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={player.user.avatar} />
<AvatarFallback>{player.user.nickname[0]}</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{player.user.nickname}</p>
<p className="text-xs text-muted-foreground">
{player.shopName ? `${player.shopName} · ` : ""}
{service.gameName}
</p>
</div>
</div>
<Separator />
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span>{service.title}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span>
¥{service.price}/{service.unit}
</span>
</div>
{service.rankRange && (
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<span>{service.rankRange}</span>
</div>
)}
</div>
</CardContent>
</Card>
<Card className="border-border/80 shadow-sm">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="quantity">{service.unit}</Label>
<Input
id="quantity"
type="number"
min={1}
max={99}
value={quantity}
onChange={(e) => setQuantity(Math.max(1, Number.parseInt(e.target.value, 10) || 1))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="note"></Label>
<Textarea
id="note"
className="rounded-lg"
placeholder="例如:希望晚上8点后开始"
value={note}
onChange={(e) => setNote(e.target.value)}
rows={3}
/>
</div>
</CardContent>
</Card>
<Card className="border-border/80 shadow-sm">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
{service.title} × {quantity}
</span>
<span>¥{totalPrice}</span>
</div>
<Separator />
<div className="flex justify-between font-medium">
<span></span>
<span className="text-lg text-primary">¥{totalPrice}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<CreditCard className="h-4 w-4" />
<span>: {balance === null ? "--" : `¥${balance.toFixed(2)}`}</span>
{balance !== null && balance < totalPrice && (
<span className="text-destructive"></span>
)}
{balance !== null && balance < totalPrice && (
<Button variant="outline" size="sm" asChild>
<Link href="/wallet"></Link>
</Button>
)}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<ShieldCheck className="h-3.5 w-3.5" />
<span></span>
</div>
</CardContent>
</Card>
<Button
className="w-full"
size="lg"
disabled={balance === null || balance < totalPrice}
onClick={() =>
requireAuth(() => {
void Promise.resolve(
createPaidOrder({
playerId: player.id,
serviceId: service.id,
shopId: player.shopId,
quantity,
note,
}),
).then((result) => {
if (!result.decision.ok) {
notifyInfo(result.decision.error.msg)
return
}
if (!result.order) return
const nextOrder = result.order
setSubmitted(true)
notifySuccess("下单成功")
setTimeout(() => {
router.push(`/order/${nextOrder.id}`)
}, 800)
})
})
}
>
¥{totalPrice}
</Button>
</div>
</div>
)
}