fix: sync notification and shop dashboard state

This commit is contained in:
zetaloop
2026-02-22 06:43:24 +08:00
parent 02269dd9c3
commit 5f25043923
6 changed files with 152 additions and 74 deletions
+12 -8
View File
@@ -1,11 +1,13 @@
"use client"
import { Bell, CheckCheck, MessageSquare, ShoppingBag } from "lucide-react" import { Bell, CheckCheck, MessageSquare, ShoppingBag } 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"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { mockNotifications } from "@/lib/mock"
import type { Notification } from "@/lib/types" import type { Notification } from "@/lib/types"
import { useNotificationStore } from "@/store/notifications"
const typeIcons: Record<Notification["type"], typeof Bell> = { const typeIcons: Record<Notification["type"], typeof Bell> = {
order: ShoppingBag, order: ShoppingBag,
@@ -46,10 +48,12 @@ function NotificationItem({ notification }: { notification: Notification }) {
} }
export default function NotificationsPage() { export default function NotificationsPage() {
const unreadCount = mockNotifications.filter((n) => !n.read).length const notifications = useNotificationStore((state) => state.notifications)
const orderNotifs = mockNotifications.filter((n) => n.type === "order") const markAllAsRead = useNotificationStore((state) => state.markAllAsRead)
const communityNotifs = mockNotifications.filter((n) => n.type === "community") const unreadCount = notifications.filter((notification) => !notification.read).length
const systemNotifs = mockNotifications.filter((n) => n.type === "system") const orderNotifs = notifications.filter((notification) => notification.type === "order")
const communityNotifs = notifications.filter((notification) => notification.type === "community")
const systemNotifs = notifications.filter((notification) => notification.type === "system")
return ( return (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
@@ -58,7 +62,7 @@ export default function NotificationsPage() {
<h1 className="text-2xl font-bold"></h1> <h1 className="text-2xl font-bold"></h1>
{unreadCount > 0 && <Badge>{unreadCount} </Badge>} {unreadCount > 0 && <Badge>{unreadCount} </Badge>}
</div> </div>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" onClick={markAllAsRead}>
<CheckCheck className="mr-1 h-4 w-4" /> <CheckCheck className="mr-1 h-4 w-4" />
</Button> </Button>
@@ -73,8 +77,8 @@ export default function NotificationsPage() {
</TabsList> </TabsList>
<TabsContent value="all" className="space-y-2 mt-4"> <TabsContent value="all" className="space-y-2 mt-4">
{mockNotifications.map((n) => ( {notifications.map((notification) => (
<NotificationItem key={n.id} notification={n} /> <NotificationItem key={notification.id} notification={notification} />
))} ))}
</TabsContent> </TabsContent>
+23 -4
View File
@@ -1,3 +1,5 @@
"use client"
import { ArrowDownLeft, ArrowUpRight, CreditCard, DollarSign } from "lucide-react" import { ArrowDownLeft, ArrowUpRight, CreditCard, DollarSign } from "lucide-react"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
@@ -9,10 +11,18 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { mockOrders, mockTransactions } from "@/lib/mock" import { mockTransactions } from "@/lib/mock"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops"
export default function ShopIncomePage() { export default function ShopIncomePage() {
const completedOrders = mockOrders.filter((o) => o.status === "completed") const userId = useAuthStore((state) => state.user?.id)
const shops = useShopStore((state) => state.shops)
const orders = useOrderStore((state) => state.orders)
const shop = shops.find((item) => item.owner.id === userId) ?? shops[0]
const shopOrders = orders.filter((order) => order.shopId === shop?.id)
const completedOrders = shopOrders.filter((o) => o.status === "completed")
const totalIncome = completedOrders.reduce((acc, order) => acc + order.totalPrice, 0) const totalIncome = completedOrders.reduce((acc, order) => acc + order.totalPrice, 0)
const currentMonth = new Date().getMonth() const currentMonth = new Date().getMonth()
@@ -20,10 +30,19 @@ export default function ShopIncomePage() {
.filter((o) => new Date(o.completedAt || "").getMonth() === currentMonth) .filter((o) => new Date(o.completedAt || "").getMonth() === currentMonth)
.reduce((acc, order) => acc + order.totalPrice, 0) .reduce((acc, order) => acc + order.totalPrice, 0)
const pendingSettlement = mockOrders const pendingSettlement = shopOrders
.filter((o) => ["in_progress", "pending_close", "pending_review"].includes(o.status)) .filter((o) => ["in_progress", "pending_close", "pending_review"].includes(o.status))
.reduce((acc, order) => acc + order.totalPrice, 0) .reduce((acc, order) => acc + order.totalPrice, 0)
const shopOrderIds = new Set(shopOrders.map((order) => order.id))
const relatedTransactions = mockTransactions.filter((transaction) => {
if (transaction.type === "withdrawal") return true
if (transaction.type !== "income") return false
const match = transaction.description.match(/ord\d+/)
if (!match) return false
return shopOrderIds.has(match[0])
})
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h1 className="text-2xl font-bold"></h1> <h1 className="text-2xl font-bold"></h1>
@@ -73,7 +92,7 @@ export default function ShopIncomePage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{mockTransactions.map((transaction) => ( {relatedTransactions.map((transaction) => (
<TableRow key={transaction.id}> <TableRow key={transaction.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+16 -6
View File
@@ -1,3 +1,5 @@
"use client"
import { AlertCircle, CheckCircle, Clock, ListOrdered } from "lucide-react" import { AlertCircle, CheckCircle, Clock, ListOrdered } 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"
@@ -12,11 +14,19 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { statusLabels } from "@/lib/constants" import { statusLabels } from "@/lib/constants"
import { mockOrders } from "@/lib/mock" import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops"
export default function ShopOrdersPage() { export default function ShopOrdersPage() {
const totalOrders = mockOrders.length const userId = useAuthStore((state) => state.user?.id)
const activeOrders = mockOrders.filter((o) => const shops = useShopStore((state) => state.shops)
const orders = useOrderStore((state) => state.orders)
const shop = shops.find((item) => item.owner.id === userId) ?? shops[0]
const shopOrders = orders.filter((order) => order.shopId === shop?.id)
const totalOrders = shopOrders.length
const activeOrders = shopOrders.filter((o) =>
[ [
"pending_payment", "pending_payment",
"pending_accept", "pending_accept",
@@ -25,8 +35,8 @@ export default function ShopOrdersPage() {
"pending_review", "pending_review",
].includes(o.status), ].includes(o.status),
).length ).length
const completedOrders = mockOrders.filter((o) => o.status === "completed").length const completedOrders = shopOrders.filter((o) => o.status === "completed").length
const disputedOrders = mockOrders.filter((o) => o.status === "disputed").length const disputedOrders = shopOrders.filter((o) => o.status === "disputed").length
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -91,7 +101,7 @@ export default function ShopOrdersPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{mockOrders.map((order) => ( {shopOrders.map((order) => (
<TableRow key={order.id}> <TableRow key={order.id}>
<TableCell className="font-medium">{order.service.title}</TableCell> <TableCell className="font-medium">{order.service.title}</TableCell>
<TableCell>{order.consumerName}</TableCell> <TableCell>{order.consumerName}</TableCell>
+80 -54
View File
@@ -8,7 +8,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { roleLabels } from "@/lib/constants" import { roleLabels } from "@/lib/constants"
import { mockGames, mockPosts } from "@/lib/mock" import { mockGames, mockOrders, mockPlayers, mockPosts } from "@/lib/mock"
export default function CommunityPage() { export default function CommunityPage() {
const [sortMode, setSortMode] = useState<"latest" | "hot">("latest") const [sortMode, setSortMode] = useState<"latest" | "hot">("latest")
@@ -74,61 +74,87 @@ export default function CommunityPage() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{filteredPosts.map((post) => ( {filteredPosts.map((post) =>
<Link key={post.id} href={`/post/${post.id}`}> (() => {
<Card className="hover:shadow-md transition-shadow"> const linkedOrder = post.linkedOrderId
<CardHeader className="pb-3"> ? mockOrders.find((order) => order.id === post.linkedOrderId)
<div className="flex items-center gap-3"> : null
<Avatar className="h-9 w-9"> const linkedPlayer = linkedOrder
<AvatarImage src={post.author.avatar} /> ? mockPlayers.find((player) => player.id === linkedOrder.playerId)
<AvatarFallback>{post.author.nickname[0]}</AvatarFallback> : null
</Avatar>
<div className="flex-1 min-w-0"> return (
<div className="flex items-center gap-2"> <Link key={post.id} href={`/post/${post.id}`}>
<span className="text-sm font-medium">{post.author.nickname}</span> <Card className="hover:shadow-md transition-shadow">
<Badge variant="outline" className="text-[10px] px-1.5 py-0"> <CardHeader className="pb-3">
{roleLabels[post.authorRole]} <div className="flex items-center gap-3">
</Badge> <Avatar className="h-9 w-9">
{post.pinned && <Pin className="h-3 w-3 text-muted-foreground" />} <AvatarImage src={post.author.avatar} />
<AvatarFallback>{post.author.nickname[0]}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{post.author.nickname}</span>
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
{roleLabels[post.authorRole]}
</Badge>
{post.pinned && <Pin className="h-3 w-3 text-muted-foreground" />}
</div>
<span className="text-xs text-muted-foreground">
{new Date(post.createdAt).toLocaleDateString("zh-CN")}
</span>
</div>
</div> </div>
<span className="text-xs text-muted-foreground"> </CardHeader>
{new Date(post.createdAt).toLocaleDateString("zh-CN")} <CardContent className="pb-3">
<h3 className="font-semibold mb-1">{post.title}</h3>
<p className="text-sm text-muted-foreground line-clamp-2">{post.content}</p>
{post.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{post.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
{post.linkedOrderId && (
<div className="mt-2 rounded border bg-muted/30 px-3 py-2 text-xs text-muted-foreground space-y-1.5">
<div className="flex items-center gap-1.5">
<ClipboardList className="h-3.5 w-3.5" />
</div>
{linkedOrder && (
<div className="pl-5">
<p>
{linkedOrder.service.gameName} · {linkedOrder.service.title}
</p>
<p>
{linkedOrder.playerName}
{linkedPlayer ? ` · ${linkedPlayer.rating}` : ""}
</p>
</div>
)}
</div>
)}
</CardContent>
<CardFooter className="pt-0 text-sm text-muted-foreground gap-4">
<span className="flex items-center gap-1">
<Heart
className={`h-4 w-4 ${post.liked ? "fill-red-500 text-red-500" : ""}`}
/>
{post.likeCount}
</span> </span>
</div> <span className="flex items-center gap-1">
</div> <MessageCircle className="h-4 w-4" />
</CardHeader> {post.commentCount}
<CardContent className="pb-3"> </span>
<h3 className="font-semibold mb-1">{post.title}</h3> </CardFooter>
<p className="text-sm text-muted-foreground line-clamp-2">{post.content}</p> </Card>
{post.tags.length > 0 && ( </Link>
<div className="flex flex-wrap gap-1 mt-2"> )
{post.tags.map((tag) => ( })(),
<Badge key={tag} variant="secondary" className="text-xs"> )}
{tag}
</Badge>
))}
</div>
)}
{post.linkedOrderId && (
<div className="mt-2 rounded border bg-muted/30 px-3 py-2 text-xs text-muted-foreground flex items-center gap-1.5">
<ClipboardList className="h-3.5 w-3.5" />
</div>
)}
</CardContent>
<CardFooter className="pt-0 text-sm text-muted-foreground gap-4">
<span className="flex items-center gap-1">
<Heart className={`h-4 w-4 ${post.liked ? "fill-red-500 text-red-500" : ""}`} />
{post.likeCount}
</span>
<span className="flex items-center gap-1">
<MessageCircle className="h-4 w-4" />
{post.commentCount}
</span>
</CardFooter>
</Card>
</Link>
))}
</div> </div>
</div> </div>
) )
+5 -2
View File
@@ -29,10 +29,11 @@ import {
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { currentUser, mockNotifications, mockShops } from "@/lib/mock" import { currentUser, mockShops } from "@/lib/mock"
import type { UserRole } from "@/lib/types" import type { UserRole } from "@/lib/types"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { useNotificationStore } from "@/store/notifications"
const roleLabels: Record<UserRole, string> = { const roleLabels: Record<UserRole, string> = {
consumer: "消费者", consumer: "消费者",
@@ -72,7 +73,9 @@ export function Header() {
setMobileOpen(false) setMobileOpen(false)
} }
const unreadCount = mockNotifications.filter((n) => !n.read).length const unreadCount = useNotificationStore(
(state) => state.notifications.filter((notification) => !notification.read).length,
)
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => { const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
+16
View File
@@ -0,0 +1,16 @@
import { create } from "zustand"
import { mockNotifications } from "@/lib/mock"
import type { Notification } from "@/lib/types"
interface NotificationState {
notifications: Notification[]
markAllAsRead: () => void
}
export const useNotificationStore = create<NotificationState>((set) => ({
notifications: mockNotifications,
markAllAsRead: () =>
set((state) => ({
notifications: state.notifications.map((notification) => ({ ...notification, read: true })),
})),
}))