Files
soul-yongping/app/admin/orders/page.tsx

276 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect, Suspense } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Search, RefreshCw, Download, Filter, TrendingUp } from "lucide-react"
import { useStore } from "@/lib/store"
interface Purchase {
id: string
userId: string
type: "section" | "fullbook" | "match"
sectionId?: string
sectionTitle?: string
amount: number
status: "pending" | "completed" | "failed"
paymentMethod?: string
referrerEarnings?: number
createdAt: string
}
function OrdersContent() {
const { getAllPurchases, getAllUsers } = useStore()
const [purchases, setPurchases] = useState<any[]>([]) // 改为 any[] 以支持新字段
const [users, setUsers] = useState<any[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState<string>("all")
const [isLoading, setIsLoading] = useState(true)
// 从API获取订单包含用户昵称
async function loadOrders() {
setIsLoading(true)
try {
const ordersRes = await fetch('/api/orders')
const ordersData = await ordersRes.json()
if (ordersData.success && ordersData.orders) {
setPurchases(ordersData.orders)
}
const usersRes = await fetch('/api/db/users')
const usersData = await usersRes.json()
if (usersData.success && usersData.users) {
setUsers(usersData.users)
}
} catch (e) {
console.error('加载订单失败', e)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
loadOrders()
}, [])
// 获取用户昵称(优先使用 order.userNickname
const getUserNickname = (order: any) => {
return order.userNickname || users.find((u: any) => u.id === order.userId)?.nickname || "匿名用户"
}
// 获取用户手机号
const getUserPhone = (userId: string) => {
const user = users.find(u => u.id === userId)
return user?.phone || "-"
}
// 格式化商品信息
const formatProduct = (order: any) => {
const type = order.productType || ""
const desc = order.description || ""
if (desc) {
if (type === "section" && desc.includes("章节")) {
if (desc.includes("-")) {
const parts = desc.split("-")
if (parts.length >= 3) {
return {
name: `${parts[1]}章 第${parts[2]}`,
type: "《一场Soul的创业实验》"
}
}
}
return { name: desc, type: "章节购买" }
}
if (type === "fullbook" || desc.includes("全书")) {
return { name: "《一场Soul的创业实验》", type: "全书购买" }
}
if (type === "match" || desc.includes("伙伴")) {
return { name: "找伙伴匹配", type: "功能服务" }
}
return { name: desc, type: "其他" }
}
if (type === "section") return { name: `章节 ${order.productId || ""}`, type: "单章" }
if (type === "fullbook") return { name: "《一场Soul的创业实验》", type: "全书" }
if (type === "match") return { name: "找伙伴匹配", type: "功能" }
return { name: "未知商品", type: type || "其他" }
}
// 过滤订单
const filteredPurchases = purchases.filter((p) => {
const product = formatProduct(p)
const matchSearch =
getUserNickname(p).includes(searchTerm) ||
getUserPhone(p.userId).includes(searchTerm) ||
product.name.includes(searchTerm) ||
(p.orderSn && p.orderSn.includes(searchTerm)) ||
(p.id && p.id.includes(searchTerm))
const matchStatus = statusFilter === "all" || p.status === statusFilter ||
(statusFilter === "completed" && p.status === "paid")
return matchSearch && matchStatus
})
// 统计数据status 可能是 'paid' 或 'completed'
const totalRevenue = purchases.filter(p => p.status === "paid" || p.status === "completed").reduce((sum, p) => sum + Number(p.amount || 0), 0)
const todayRevenue = purchases
.filter(p => {
const today = new Date().toDateString()
return (p.status === "paid" || p.status === "completed") && new Date(p.createdAt).toDateString() === today
})
.reduce((sum, p) => sum + Number(p.amount || 0), 0)
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-white"></h2>
<p className="text-gray-400 mt-1"> {purchases.length} </p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-400">:</span>
<span className="text-[#38bdac] font-bold">¥{totalRevenue.toFixed(2)}</span>
<span className="text-gray-600">|</span>
<span className="text-gray-400">:</span>
<span className="text-[#FFD700] font-bold">¥{todayRevenue.toFixed(2)}</span>
</div>
</div>
</div>
<div className="flex items-center gap-4 mb-6">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<Input
type="text"
placeholder="搜索订单号/用户/章节..."
className="pl-10 bg-[#0f2137] border-gray-700 text-white placeholder:text-gray-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="bg-[#0f2137] border border-gray-700 text-white rounded-lg px-3 py-2 text-sm"
>
<option value="all"></option>
<option value="completed"></option>
<option value="pending"></option>
<option value="created"></option>
<option value="failed"></option>
</select>
</div>
<Button
variant="outline"
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardContent className="p-0">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
<span className="ml-2 text-gray-400">...</span>
</div>
) : (
<Table>
<TableHeader>
<TableRow className="bg-[#0a1628] hover:bg-[#0a1628] border-gray-700">
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
<TableHead className="text-gray-400"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredPurchases.map((purchase) => {
const product = formatProduct(purchase)
return (
<TableRow key={purchase.id} className="hover:bg-[#0a1628] border-gray-700/50">
<TableCell className="font-mono text-xs text-gray-400">
{(purchase.orderSn || purchase.id || "").slice(0, 12)}...
</TableCell>
<TableCell>
<div>
<p className="text-white text-sm">{getUserNickname(purchase)}</p>
<p className="text-gray-500 text-xs">{getUserPhone(purchase.userId)}</p>
</div>
</TableCell>
<TableCell>
<div>
<p className="text-white text-sm">{product.name}</p>
<p className="text-gray-500 text-xs">{product.type}</p>
</div>
</TableCell>
<TableCell className="text-[#38bdac] font-bold">
¥{Number(purchase.amount || 0).toFixed(2)}
</TableCell>
<TableCell className="text-gray-300">
{purchase.paymentMethod === "wechat" ? "微信支付" :
purchase.paymentMethod === "alipay" ? "支付宝" :
purchase.paymentMethod || "微信支付"}
</TableCell>
<TableCell>
{purchase.status === "paid" || purchase.status === "completed" ? (
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
</Badge>
) : purchase.status === "pending" || purchase.status === "created" ? (
<Badge className="bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/20 border-0">
</Badge>
) : (
<Badge className="bg-red-500/20 text-red-400 hover:bg-red-500/20 border-0">
</Badge>
)}
</TableCell>
<TableCell className="text-[#FFD700]">
{purchase.referrerEarnings ? `¥${Number(purchase.referrerEarnings).toFixed(2)}` : "-"}
</TableCell>
<TableCell className="text-gray-400 text-sm">
{new Date(purchase.createdAt).toLocaleString('zh-CN')}
</TableCell>
</TableRow>
)
})}
{filteredPurchases.length === 0 && (
<TableRow>
<TableCell colSpan={8} className="text-center py-12 text-gray-500">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}
export default function OrdersPage() {
return (
<Suspense fallback={null}>
<OrdersContent />
</Suspense>
)
}