Files
juwan-frontend/app/(dashboard)/dashboard/services/new/page.tsx
T
2026-04-25 14:31:04 +08:00

310 lines
10 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 { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import {
createPlayerService,
getGameById,
getServiceById,
listGames,
listPlayers,
updatePlayerService,
} from "@/lib/api"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
import { toApiError } from "@/lib/errors"
import { GameIcon } from "@/lib/game-icons"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { Game, PlayerService } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useShopStore } from "@/store/shops"
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { useForm, useWatch } from "react-hook-form"
import { z } from "zod"
const serviceSchema = z.object({
gameId: z.string().min(1, "请选择游戏"),
title: z.string().min(2, "标题至少2个字符"),
description: z.string().min(10, "描述至少10个字符"),
price: z.string().min(1, "请输入价格"),
unit: z.string().min(1, "请输入单位"),
rankRange: z.string().optional(),
availability: z.string().min(1, "请输入可用时间"),
})
export default function NewServicePage() {
const router = useRouter()
const searchParams = useSearchParams()
const serviceId = searchParams.get("serviceId")
const userId = useAuthStore((state) => state.user?.id)
const currentRole = useAuthStore((state) => state.currentRole)
const shops = useShopStore((state) => state.shops)
const [players, setPlayers] = useState<Awaited<ReturnType<typeof listPlayers>>>([])
const [editingService, setEditingService] = useState<PlayerService | undefined>(undefined)
const [loadingService, setLoadingService] = useState(Boolean(serviceId))
const ownerShop = resolveOwnerShop(userId, shops)
const scopedPlayerIds =
currentRole === "player"
? userId
? players.filter((player) => player.user.id === userId).map((player) => player.id)
: []
: currentRole === "owner"
? ownerShop
? players.filter((player) => player.shopId === ownerShop.id).map((player) => player.id)
: []
: []
const scopedPlayerIdSet = new Set(scopedPlayerIds)
const targetPlayerId = editingService?.playerId ?? scopedPlayerIds[0]
const {
register,
handleSubmit,
setValue,
control,
formState: { errors, isSubmitting },
} = useForm<z.infer<typeof serviceSchema>>({
resolver: standardSchemaResolver(serviceSchema),
defaultValues: {
gameId: "",
title: "",
description: "",
price: "",
unit: "",
rankRange: "",
availability: "",
},
})
useEffect(() => {
let cancelled = false
listPlayers()
.then((items) => {
if (!cancelled) setPlayers(items)
})
.catch((error) => {
if (!cancelled) notifyInfo(toApiError(error).msg)
})
return () => {
cancelled = true
}
}, [])
useEffect(() => {
if (!serviceId) return
let cancelled = false
getServiceById(serviceId)
.then((service) => {
if (!cancelled) setEditingService(service)
})
.catch((error) => {
if (!cancelled) notifyInfo(toApiError(error).msg)
})
.finally(() => {
if (!cancelled) setLoadingService(false)
})
return () => {
cancelled = true
}
}, [serviceId])
useEffect(() => {
if (!editingService) return
setValue("gameId", editingService.gameId)
setValue("title", editingService.title)
setValue("description", editingService.description)
setValue("price", editingService.price.toString())
setValue("unit", editingService.unit)
setValue("rankRange", editingService.rankRange ?? "")
setValue("availability", editingService.availability.join("、"))
}, [editingService, setValue])
const selectedGameId = useWatch({ control, name: "gameId" })
const selectedUnit = useWatch({ control, name: "unit" })
const [games, setGames] = useState<Game[]>([])
useEffect(() => {
let cancelled = false
listGames()
.then((items) => {
if (cancelled) return
setGames(items)
})
.catch(() => {
if (cancelled) return
setGames([])
})
return () => {
cancelled = true
}
}, [])
if (loadingService) {
return <div className="text-sm text-muted-foreground">...</div>
}
if (serviceId && (!editingService || !scopedPlayerIdSet.has(editingService.playerId))) {
return <div className="text-sm text-muted-foreground"></div>
}
if (!targetPlayerId) {
return <div className="text-sm text-muted-foreground"></div>
}
const onSubmit = async (data: z.infer<typeof serviceSchema>) => {
const game = await getGameById(data.gameId)
if (!game) return
const payload: Omit<PlayerService, "id"> = {
playerId: targetPlayerId,
gameId: game.id,
gameName: game.name,
title: data.title,
description: data.description,
price: Number(data.price),
unit: data.unit as PlayerService["unit"],
rankRange: data.rankRange?.trim() ? data.rankRange.trim() : undefined,
availability: data.availability
.split("、")
.map((item) => item.trim())
.filter(Boolean),
}
if (editingService) {
await updatePlayerService(editingService.id, payload)
notifySuccess("服务已保存")
} else {
await createPlayerService(payload)
notifySuccess("服务已发布")
}
router.push("/dashboard/services")
}
return (
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-8">
<Link
href="/dashboard/services"
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
</Link>
<Card className="hover:shadow-card-hover">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-2">
<Label></Label>
<Select value={selectedGameId} onValueChange={(value) => setValue("gameId", value)}>
<SelectTrigger>
<SelectValue placeholder="选择游戏" />
</SelectTrigger>
<SelectContent>
{games.map((game) => (
<SelectItem key={game.id} value={game.id}>
<div className="flex items-center gap-2">
<GameIcon name={game.icon} className="h-4 w-4" />
{game.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="title"></Label>
<Input id="title" placeholder="例如:英雄联盟上分陪玩" {...register("title")} />
{errors.title && <p className="text-xs text-destructive">{errors.title.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
placeholder="详细描述你的服务内容、优势等"
rows={4}
{...register("description")}
/>
{errors.description && (
<p className="text-xs text-destructive">{errors.description.message}</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="price"></Label>
<Input id="price" type="number" placeholder="30" {...register("price")} />
{errors.price && <p className="text-xs text-destructive">{errors.price.message}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="unit"></Label>
<Select value={selectedUnit} onValueChange={(value) => setValue("unit", value)}>
<SelectTrigger>
<SelectValue placeholder="选择单位" />
</SelectTrigger>
<SelectContent>
<SelectItem value="局"></SelectItem>
<SelectItem value="小时"></SelectItem>
<SelectItem value="星"></SelectItem>
<SelectItem value="次"></SelectItem>
<SelectItem value="段"></SelectItem>
</SelectContent>
</Select>
{errors.unit && <p className="text-xs text-destructive">{errors.unit.message}</p>}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="rankRange"></Label>
<Input id="rankRange" placeholder="例如:钻石-大师" {...register("rankRange")} />
</div>
<div className="space-y-2">
<Label htmlFor="availability"></Label>
<Input
id="availability"
placeholder="例如:周一至周五 19:00-23:00"
{...register("availability")}
/>
{errors.availability && (
<p className="text-xs text-destructive">{errors.availability.message}</p>
)}
</div>
<div className="flex gap-2">
<Button type="submit" className="flex-1" disabled={isSubmitting}>
{isSubmitting ? "发布中..." : "发布服务"}
</Button>
<Button type="button" variant="outline" asChild>
<Link href="/dashboard/services"></Link>
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}