feat: make dashboard service management actionable
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
|
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
|
||||||
import { ArrowLeft } from "lucide-react"
|
import { ArrowLeft } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { useEffect } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -20,8 +21,12 @@ import {
|
|||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { GameIcon } from "@/lib/game-icons"
|
import { GameIcon } from "@/lib/game-icons"
|
||||||
import { mockGames } from "@/lib/mock"
|
import { mockGames } from "@/lib/mock"
|
||||||
|
import type { PlayerService } from "@/lib/types"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
import { useServiceStore } from "@/store/services"
|
||||||
|
|
||||||
const serviceSchema = z.object({
|
const serviceSchema = z.object({
|
||||||
|
gameId: z.string().min(1, "请选择游戏"),
|
||||||
title: z.string().min(2, "标题至少2个字符"),
|
title: z.string().min(2, "标题至少2个字符"),
|
||||||
description: z.string().min(10, "描述至少10个字符"),
|
description: z.string().min(10, "描述至少10个字符"),
|
||||||
price: z.string().min(1, "请输入价格"),
|
price: z.string().min(1, "请输入价格"),
|
||||||
@@ -32,17 +37,71 @@ const serviceSchema = z.object({
|
|||||||
|
|
||||||
export default function NewServicePage() {
|
export default function NewServicePage() {
|
||||||
const router = useRouter()
|
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 {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
|
watch,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm({
|
} = useForm<z.infer<typeof serviceSchema>>({
|
||||||
resolver: standardSchemaResolver(serviceSchema),
|
resolver: standardSchemaResolver(serviceSchema),
|
||||||
|
defaultValues: {
|
||||||
|
gameId: "",
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
price: "",
|
||||||
|
unit: "",
|
||||||
|
rankRange: "",
|
||||||
|
availability: "",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = async () => {
|
useEffect(() => {
|
||||||
await new Promise((r) => setTimeout(r, 500))
|
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 = watch("gameId")
|
||||||
|
const selectedUnit = watch("unit")
|
||||||
|
|
||||||
|
const onSubmit = async (data: z.infer<typeof serviceSchema>) => {
|
||||||
|
const game = mockGames.find((item) => item.id === 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")
|
router.push("/dashboard/services")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +123,7 @@ export default function NewServicePage() {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>游戏</Label>
|
<Label>游戏</Label>
|
||||||
<Select>
|
<Select value={selectedGameId} onValueChange={(value) => setValue("gameId", value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择游戏" />
|
<SelectValue placeholder="选择游戏" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -108,7 +167,7 @@ export default function NewServicePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="unit">单位</Label>
|
<Label htmlFor="unit">单位</Label>
|
||||||
<Select onValueChange={(value) => setValue("unit", value)}>
|
<Select value={selectedUnit} onValueChange={(value) => setValue("unit", value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择单位" />
|
<SelectValue placeholder="选择单位" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import { Edit, Plus, Trash2 } from "lucide-react"
|
import { Edit, Plus, Trash2 } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
@@ -11,9 +13,12 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { mockServices } from "@/lib/mock"
|
import { useServiceStore } from "@/store/services"
|
||||||
|
|
||||||
export default function ServicesPage() {
|
export default function ServicesPage() {
|
||||||
|
const services = useServiceStore((state) => state.services)
|
||||||
|
const deleteService = useServiceStore((state) => state.deleteService)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -43,7 +48,7 @@ export default function ServicesPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{mockServices.map((service) => (
|
{services.map((service) => (
|
||||||
<TableRow key={service.id}>
|
<TableRow key={service.id}>
|
||||||
<TableCell className="font-medium">{service.title}</TableCell>
|
<TableCell className="font-medium">{service.title}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -63,11 +68,11 @@ export default function ServicesPage() {
|
|||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end gap-1">
|
<div className="flex justify-end gap-1">
|
||||||
<Button variant="ghost" size="icon" asChild>
|
<Button variant="ghost" size="icon" asChild>
|
||||||
<Link href="/dashboard/services/new">
|
<Link href={`/dashboard/services/new?serviceId=${service.id}`}>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon" onClick={() => deleteService(service.id)}>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { create } from "zustand"
|
||||||
|
import { mockServices } from "@/lib/mock"
|
||||||
|
import type { PlayerService } from "@/lib/types"
|
||||||
|
|
||||||
|
interface ServiceState {
|
||||||
|
services: PlayerService[]
|
||||||
|
createService: (service: Omit<PlayerService, "id">) => void
|
||||||
|
updateService: (serviceId: string, patch: Partial<Omit<PlayerService, "id">>) => void
|
||||||
|
deleteService: (serviceId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useServiceStore = create<ServiceState>((set) => ({
|
||||||
|
services: mockServices,
|
||||||
|
createService: (service) =>
|
||||||
|
set((state) => ({
|
||||||
|
services: [
|
||||||
|
...state.services,
|
||||||
|
{
|
||||||
|
...service,
|
||||||
|
id: `s${Date.now()}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
updateService: (serviceId, patch) =>
|
||||||
|
set((state) => ({
|
||||||
|
services: state.services.map((service) =>
|
||||||
|
service.id === serviceId ? { ...service, ...patch } : service,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
deleteService: (serviceId) =>
|
||||||
|
set((state) => ({
|
||||||
|
services: state.services.filter((service) => service.id !== serviceId),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user