feat: dashboard overview, services list, and service publish pages

This commit is contained in:
zetaloop
2026-02-20 15:25:34 +08:00
parent e132ffcefb
commit 8f4e8604d3
3 changed files with 365 additions and 9 deletions
+146 -3
View File
@@ -1,8 +1,151 @@
"use client"
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useForm } 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 { mockGames } from "@/lib/mock-data"
const serviceSchema = z.object({
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 {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: standardSchemaResolver(serviceSchema),
})
const onSubmit = async () => {
await new Promise((r) => setTimeout(r, 500))
router.push("/dashboard/services")
}
return (
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="mt-2 text-muted-foreground"></p>
<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>
<SelectTrigger>
<SelectValue placeholder="选择游戏" />
</SelectTrigger>
<SelectContent>
{mockGames.map((game) => (
<SelectItem key={game.id} value={game.id}>
{game.icon} {game.name}
</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>
<SelectTrigger>
<SelectValue placeholder="选择单位" />
</SelectTrigger>
<SelectContent>
<SelectItem value="局"></SelectItem>
<SelectItem value="小时"></SelectItem>
<SelectItem value="星"></SelectItem>
<SelectItem value="次"></SelectItem>
<SelectItem value="段"></SelectItem>
</SelectContent>
</Select>
</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>
)
}
+78 -3
View File
@@ -1,8 +1,83 @@
import { Edit, Plus, Trash2 } from "lucide-react"
import Link from "next/link"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { mockServices } from "@/lib/mock-data"
export default function ServicesPage() {
return (
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="mt-2 text-muted-foreground"></p>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<Button asChild>
<Link href="/dashboard/services/new">
<Plus className="mr-1 h-4 w-4" />
</Link>
</Button>
</div>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockServices.map((service) => (
<TableRow key={service.id}>
<TableCell className="font-medium">{service.title}</TableCell>
<TableCell>
<Badge variant="secondary">{service.gameName}</Badge>
</TableCell>
<TableCell>
¥{service.price}/{service.unit}
</TableCell>
<TableCell>{service.rankRange ?? "-"}</TableCell>
<TableCell>
<div className="text-xs text-muted-foreground">
{service.availability.map((a) => (
<div key={a}>{a}</div>
))}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
<Button variant="ghost" size="icon" asChild>
<Link href="/dashboard/services/new">
<Edit className="h-4 w-4" />
</Link>
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}