feat: add shop dashboard order overview, income stats, and rule settings pages
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
"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 { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -15,6 +25,9 @@ const ownerLinks = [
|
||||
{ href: "/dashboard", label: "概览", icon: LayoutDashboard },
|
||||
{ href: "/dashboard/services", label: "服务管理", icon: ListOrdered },
|
||||
{ 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/templates", label: "模板编辑", icon: Palette },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user