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
+141 -3
View File
@@ -1,8 +1,146 @@
"use client"
import { CheckCircle, Clock, DollarSign, ListOrdered, Star, TrendingUp, Users } 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 { Progress } from "@/components/ui/progress"
import { mockOrders, mockPlayers, mockServices, mockShops } from "@/lib/mock-data"
import { useAuthStore } from "@/store/auth"
const statusLabels: Record<string, string> = {
pending_payment: "待支付",
pending_accept: "待接单",
in_progress: "进行中",
pending_close: "待结单",
pending_review: "待评价",
disputed: "争议中",
completed: "已完成",
cancelled: "已取消",
}
export default function DashboardPage() { export default function DashboardPage() {
const { currentRole } = useAuthStore()
const isOwner = currentRole === "owner"
const player = mockPlayers[0]
const shop = mockShops[0]
const recentOrders = mockOrders.slice(0, 3)
return ( return (
<div> <div className="space-y-6">
<h1 className="text-2xl font-bold"></h1> <h1 className="text-2xl font-bold"></h1>
<p className="mt-2 text-muted-foreground"></p>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<ListOrdered className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isOwner ? shop.totalOrders : player.totalOrders}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Star className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isOwner ? shop.rating : player.rating}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{isOwner ? "签约打手" : "完成率"}</CardTitle>
{isOwner ? (
<Users className="h-4 w-4 text-muted-foreground" />
) : (
<CheckCircle className="h-4 w-4 text-muted-foreground" />
)}
</CardHeader>
<CardContent>
{isOwner ? (
<div className="text-2xl font-bold">{shop.playerCount}</div>
) : (
<div className="space-y-2">
<div className="text-2xl font-bold">
{(player.completionRate * 100).toFixed(0)}%
</div>
<Progress value={player.completionRate * 100} className="h-1.5" />
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{isOwner ? "本月收入" : "服务数"}</CardTitle>
{isOwner ? (
<DollarSign className="h-4 w-4 text-muted-foreground" />
) : (
<TrendingUp className="h-4 w-4 text-muted-foreground" />
)}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{isOwner ? "¥12,800" : mockServices.filter((s) => s.playerId === player.id).length}
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base"></CardTitle>
<Button variant="ghost" size="sm" asChild>
<Link href="/orders"></Link>
</Button>
</CardHeader>
<CardContent>
<div className="space-y-3">
{recentOrders.map((order) => (
<Link key={order.id} href={`/order/${order.id}`}>
<div className="flex items-center justify-between rounded-md border p-3 hover:bg-muted/50 transition-colors">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{order.service.title}</p>
<p className="text-xs text-muted-foreground">
{order.consumerName} {order.playerName}
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className="text-sm font-medium">¥{order.totalPrice}</span>
<Badge variant="outline" className="text-xs">
{statusLabels[order.status]}
</Badge>
</div>
</div>
</Link>
))}
</div>
</CardContent>
</Card>
{!isOwner && (
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="flex gap-2 flex-wrap">
<Button asChild>
<Link href="/dashboard/services/new"></Link>
</Button>
<Button variant="outline" asChild>
<Link href="/orders">
<Clock className="mr-1 h-4 w-4" />
</Link>
</Button>
</CardContent>
</Card>
)}
</div> </div>
) )
} }
+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() { 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 ( return (
<div> <div className="max-w-2xl space-y-4">
<h1 className="text-2xl font-bold"></h1> <Link
<p className="mt-2 text-muted-foreground"></p> 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> </div>
) )
} }
+77 -2
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() { export default function ServicesPage() {
return ( return (
<div> <div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1> <h1 className="text-2xl font-bold"></h1>
<p className="mt-2 text-muted-foreground"></p> <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> </div>
) )
} }