feat: add shop dashboard order overview, income stats, and rule settings pages

This commit is contained in:
zetaloop
2026-02-20 23:04:38 +08:00
parent 1362a29755
commit 7e632ce092
4 changed files with 386 additions and 1 deletions
@@ -0,0 +1,112 @@
import { ArrowDownLeft, ArrowUpRight, CreditCard, DollarSign } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { mockOrders, mockTransactions } from "@/lib/mock-data"
export default function ShopIncomePage() {
const completedOrders = mockOrders.filter((o) => o.status === "completed")
const totalIncome = completedOrders.reduce((acc, order) => acc + order.totalPrice, 0)
const currentMonth = new Date().getMonth()
const thisMonthIncome = completedOrders
.filter((o) => new Date(o.completedAt || "").getMonth() === currentMonth)
.reduce((acc, order) => acc + order.totalPrice, 0)
const pendingSettlement = mockOrders
.filter((o) => ["in_progress", "pending_close", "pending_review"].includes(o.status))
.reduce((acc, order) => acc + order.totalPrice, 0)
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold"></h1>
<div className="grid gap-4 sm:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">¥{totalIncome.toFixed(2)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">¥{thisMonthIncome.toFixed(2)}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<ArrowUpRight className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">¥{pendingSettlement.toFixed(2)}</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockTransactions.map((transaction) => (
<TableRow key={transaction.id}>
<TableCell>
<div className="flex items-center gap-2">
{transaction.amount > 0 ? (
<ArrowDownLeft className="h-4 w-4 text-green-500" />
) : (
<ArrowUpRight className="h-4 w-4 text-red-500" />
)}
<Badge variant={transaction.amount > 0 ? "default" : "secondary"}>
{transaction.type === "topup"
? "充值"
: transaction.type === "payment"
? "支付"
: transaction.type === "income"
? "收入"
: transaction.type === "withdrawal"
? "提现"
: "退款"}
</Badge>
</div>
</TableCell>
<TableCell>{transaction.description}</TableCell>
<TableCell className={transaction.amount > 0 ? "text-green-600" : "text-red-600"}>
{transaction.amount > 0 ? "+" : ""}
{transaction.amount}
</TableCell>
<TableCell>{new Date(transaction.createdAt).toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,117 @@
import { AlertCircle, CheckCircle, Clock, ListOrdered } 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 { statusLabels } from "@/lib/constants"
import { mockOrders } from "@/lib/mock-data"
export default function ShopOrdersPage() {
const totalOrders = mockOrders.length
const activeOrders = mockOrders.filter((o) =>
[
"pending_payment",
"pending_accept",
"in_progress",
"pending_close",
"pending_review",
].includes(o.status),
).length
const completedOrders = mockOrders.filter((o) => o.status === "completed").length
const disputedOrders = mockOrders.filter((o) => o.status === "disputed").length
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
</div>
<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">{totalOrders}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{activeOrders}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{completedOrders}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<AlertCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{disputedOrders}</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockOrders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-medium">{order.service.title}</TableCell>
<TableCell>{order.consumerName}</TableCell>
<TableCell>{order.playerName}</TableCell>
<TableCell>
<Badge variant="outline">{statusLabels[order.status]}</Badge>
</TableCell>
<TableCell>¥{order.totalPrice}</TableCell>
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" asChild>
<Link href={`/order/${order.id}`}></Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,143 @@
"use client"
import { Save } from "lucide-react"
import { useState } from "react"
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 { Switch } from "@/components/ui/switch"
import { mockShops } from "@/lib/mock-data"
export default function ShopRulesPage() {
const shop = mockShops[0]
const [allowMultiShop, setAllowMultiShop] = useState(shop.allowMultiShop)
const [allowIndependentOrders, setAllowIndependentOrders] = useState(shop.allowIndependentOrders)
const [dispatchMode, setDispatchMode] = useState(shop.dispatchMode)
const [commissionType, setCommissionType] = useState(shop.commissionType)
const [commissionValue, setCommissionValue] = useState(shop.commissionValue.toString())
const handleSave = () => {
alert("设置已保存")
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<Button onClick={handleSave}>
<Save className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="allow-multi-shop" className="flex flex-col space-y-1">
<span></span>
<span className="font-normal text-sm text-muted-foreground">
</span>
</Label>
<Switch
id="allow-multi-shop"
checked={allowMultiShop}
onCheckedChange={setAllowMultiShop}
/>
</div>
<div className="flex items-center justify-between space-x-2">
<Label htmlFor="allow-independent" className="flex flex-col space-y-1">
<span></span>
<span className="font-normal text-sm text-muted-foreground">
</span>
</Label>
<Switch
id="allow-independent"
checked={allowIndependentOrders}
onCheckedChange={setAllowIndependentOrders}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Select
value={dispatchMode}
onValueChange={(v: "manual" | "auto") => setDispatchMode(v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual"></SelectItem>
<SelectItem value="auto"></SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label></Label>
<Select
value={commissionType}
onValueChange={(v: "fixed" | "percentage") => setCommissionType(v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fixed"></SelectItem>
<SelectItem value="percentage"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<div className="relative">
<Input
type="number"
value={commissionValue}
onChange={(e) => setCommissionValue(e.target.value)}
/>
<div className="absolute right-3 top-2.5 text-sm text-muted-foreground">
{commissionType === "percentage" ? "%" : "元"}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}
+14 -1
View File
@@ -1,6 +1,16 @@
"use client" "use client"
import { Gamepad2, LayoutDashboard, ListOrdered, Palette, Store, Users } from "lucide-react" import {
ClipboardList,
Gamepad2,
LayoutDashboard,
ListOrdered,
Palette,
Settings,
Store,
TrendingUp,
Users,
} from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -15,6 +25,9 @@ const ownerLinks = [
{ href: "/dashboard", label: "概览", icon: LayoutDashboard }, { href: "/dashboard", label: "概览", icon: LayoutDashboard },
{ href: "/dashboard/services", label: "服务管理", icon: ListOrdered }, { href: "/dashboard/services", label: "服务管理", icon: ListOrdered },
{ href: "/dashboard/shop", label: "店铺管理", icon: Store }, { href: "/dashboard/shop", label: "店铺管理", icon: Store },
{ href: "/dashboard/shop/orders", label: "订单总览", icon: ClipboardList },
{ href: "/dashboard/shop/income", label: "收入统计", icon: TrendingUp },
{ href: "/dashboard/shop/rules", label: "规则设置", icon: Settings },
{ href: "/dashboard/shop/employees", label: "员工管理", icon: Users }, { href: "/dashboard/shop/employees", label: "员工管理", icon: Users },
{ href: "/dashboard/shop/templates", label: "模板编辑", icon: Palette }, { href: "/dashboard/shop/templates", label: "模板编辑", icon: Palette },
] ]