519fb92c34
Turn on react-hooks/set-state-in-effect and react-hooks/incompatible-library, then remove effect-driven local state sync patterns across affected pages. Keep behavior stable by deriving values from source state, remounting tab state by role key, and replacing useForm watch with useWatch.
218 lines
7.9 KiB
TypeScript
218 lines
7.9 KiB
TypeScript
"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 { GameIcon } from "@/lib/game-icons"
|
||
import type { PlayerService } from "@/lib/types"
|
||
import { useAuthStore } from "@/store/auth"
|
||
import { useServiceStore } from "@/store/services"
|
||
|
||
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 services = useServiceStore((state) => state.services)
|
||
const createService = useServiceStore((state) => state.createService)
|
||
const updateService = useServiceStore((state) => state.updateService)
|
||
const editingService = services.find((service) => service.id === serviceId)
|
||
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()
|
||
|
||
const onSubmit = async (data: z.infer<typeof serviceSchema>) => {
|
||
const game = getGameById(data.gameId)
|
||
if (!game) return
|
||
|
||
const payload: Omit<PlayerService, "id"> = {
|
||
playerId: editingService?.playerId ?? userId ?? "u5",
|
||
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="max-w-2xl space-y-4">
|
||
<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>
|
||
<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>
|
||
)
|
||
}
|