Files
juwan-frontend/app/(dashboard)/dashboard/services/new/page.tsx
T
2026-02-25 05:27:17 +08:00

247 lines
9.1 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 { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { useEffect } from "react"
import { useForm, useWatch } from "react-hook-form"
import { z } from "zod"
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 { getGameById, listGames } from "@/lib/api"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
import { GameIcon } from "@/lib/game-icons"
import type { PlayerService } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { usePlayerStore } from "@/store/players"
import { useServiceStore } from "@/store/services"
import { useShopStore } from "@/store/shops"
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 = usePlayerStore((state) => state.players)
const services = useServiceStore((state) => state.services)
const createService = useServiceStore((state) => state.createService)
const updateService = useServiceStore((state) => state.updateService)
const ownerShop = resolveOwnerShop(userId, shops)
const scopedPlayerIds =
currentRole === "player"
? userId
? [userId]
: []
: currentRole === "owner"
? ownerShop
? players.filter((player) => player.shopId === ownerShop.id).map((player) => player.id)
: []
: []
const scopedPlayerIdSet = new Set(scopedPlayerIds)
const editingService = services.find(
(service) => service.id === serviceId && scopedPlayerIdSet.has(service.playerId),
)
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(() => {
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 = listGames()
if (serviceId && !editingService) {
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 = 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) {
updateService(editingService.id, payload)
} else {
createService(payload)
}
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-[var(--shadow-card)]">
<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>
)
}