Add new files

#VERCEL_SKIP

Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
This commit is contained in:
v0
2026-01-04 05:02:13 +00:00
parent 203955500e
commit ecae8fcef8
80 changed files with 6533 additions and 484 deletions

70
app/account-help/page.tsx Normal file
View File

@@ -0,0 +1,70 @@
"use client"
import { useRouter } from "next/navigation"
import { ArrowLeft, Shield, Phone, HelpCircle, ChevronRight, Lock, Mail, FileText, MessageSquare } from "lucide-react"
export default function AccountHelpPage() {
const router = useRouter()
const sections = [
{
title: "账号安全",
items: [
{ icon: Lock, label: "修改密码", desc: "定期修改密码保障账号安全" },
{ icon: Phone, label: "绑定手机", desc: "已绑定: 138****8888" },
{ icon: Mail, label: "绑定邮箱", desc: "未绑定" },
{ icon: Shield, label: "实名认证", desc: "已认证" },
],
},
{
title: "帮助中心",
items: [
{ icon: HelpCircle, label: "常见问题", desc: "查看热门问题解答" },
{ icon: FileText, label: "用户协议", desc: "查看服务条款" },
{ icon: FileText, label: "隐私政策", desc: "了解隐私保护" },
{ icon: MessageSquare, label: "意见反馈", desc: "告诉我们您的建议" },
],
},
]
return (
<div className="min-h-screen bg-background pb-24">
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
<button onClick={() => router.back()} className="text-white/70 hover:text-white">
<ArrowLeft size={20} />
</button>
<h1 className="font-bold text-lg"></h1>
</header>
<div className="p-4 space-y-6">
{sections.map((section, sectionIndex) => (
<div key={sectionIndex}>
<h2 className="text-sm font-bold text-white/60 mb-3 px-1">{section.title}</h2>
<div className="bg-card border border-border rounded-xl overflow-hidden">
{section.items.map((item, itemIndex) => (
<button
key={itemIndex}
className="w-full flex items-center gap-3 p-4 hover:bg-white/5 transition-colors border-b border-border last:border-0"
>
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<item.icon size={18} className="text-primary" />
</div>
<div className="flex-1 text-left">
<div className="font-medium text-sm">{item.label}</div>
<div className="text-xs text-white/40">{item.desc}</div>
</div>
<ChevronRight size={16} className="text-white/30" />
</button>
))}
</div>
</div>
))}
<div className="text-center pt-4">
<p className="text-xs text-white/30"> v1.0.0</p>
<p className="text-xs text-white/20 mt-1">© 2025 All Rights Reserved</p>
</div>
</div>
</div>
)
}

3
app/admin/loading.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

280
app/admin/mall/client.tsx Normal file
View File

@@ -0,0 +1,280 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { ArrowLeft, Search, Plus, Edit, Trash2, Eye, RefreshCw } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { db, type Product } from "@/lib/db/schema"
export default function AdminMallClient() {
const router = useRouter()
const [products, setProducts] = useState<Product[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [categoryFilter, setCategoryFilter] = useState("all")
const [loading, setLoading] = useState(true)
const [showAddDialog, setShowAddDialog] = useState(false)
const [newProduct, setNewProduct] = useState({ name: "", category: "avatar", price: 0, description: "" })
useEffect(() => {
loadProducts()
}, [])
const loadProducts = async () => {
setLoading(true)
try {
const allProducts = await db.products.toArray()
setProducts(allProducts)
} catch (error) {
console.error("Failed to load products:", error)
}
setLoading(false)
}
const filteredProducts = products.filter((product) => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase())
const matchesCategory = categoryFilter === "all" || product.category === categoryFilter
return matchesSearch && matchesCategory
})
const deleteProduct = async (productId: number) => {
if (confirm("确定要删除该商品吗?")) {
await db.products.delete(productId)
loadProducts()
}
}
const addProduct = async () => {
if (!newProduct.name || newProduct.price <= 0) {
alert("请填写商品名称和价格")
return
}
await db.products.add({
name: newProduct.name,
category: newProduct.category as any,
price: newProduct.price,
originalPrice: Math.floor(newProduct.price * 1.2),
description: newProduct.description,
image: `/placeholder.svg?height=200&width=200&query=${newProduct.name}`,
stock: 999,
sales: 0,
rating: 5.0,
tags: [newProduct.category],
isHot: false,
isNew: true,
createdAt: new Date(),
})
setShowAddDialog(false)
setNewProduct({ name: "", category: "avatar", price: 0, description: "" })
loadProducts()
}
const categories = [
{ value: "all", label: "全部分类" },
{ value: "avatar", label: "头像框" },
{ value: "gift", label: "礼物" },
{ value: "vip", label: "VIP特权" },
{ value: "skin", label: "皮肤" },
{ value: "points", label: "游戏点卡" },
]
return (
<div className="min-h-screen bg-slate-900 p-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => router.push("/admin")}
className="text-slate-400 hover:text-white"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="text-2xl font-bold text-white"></h1>
</div>
<Card className="bg-slate-800/50 border-slate-700 mb-6">
<CardContent className="p-4">
<div className="flex flex-wrap gap-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<Input
placeholder="搜索商品名称..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-slate-700/50 border-slate-600 text-white pl-10"
/>
</div>
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-[150px] bg-slate-700/50 border-slate-600 text-white">
<SelectValue placeholder="商品分类" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
{categories.map((cat) => (
<SelectItem key={cat.value} value={cat.value}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" onClick={loadProducts} className="border-slate-600 text-slate-300 bg-transparent">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-cyan-500 hover:bg-cyan-600" onClick={() => setShowAddDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">{products.length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-green-400">{products.filter((p) => (p.stock || 0) > 0).length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-amber-400">{products.filter((p) => p.isHot).length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-cyan-400">
{products.reduce((sum, p) => sum + (p.sales || 0), 0).toLocaleString()}
</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
</div>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"> ({filteredProducts.length})</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8 text-slate-400">...</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{filteredProducts.map((product) => (
<Card key={product.id} className="bg-slate-700/50 border-slate-600 overflow-hidden group">
<div className="relative aspect-square">
<img
src={product.image || "/placeholder.svg"}
alt={product.name}
className="w-full h-full object-cover"
/>
{product.isHot && <Badge className="absolute top-2 left-2 bg-red-500"></Badge>}
{product.isNew && <Badge className="absolute top-2 right-2 bg-green-500"></Badge>}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button size="icon" variant="ghost" className="text-white hover:bg-white/20">
<Eye className="w-4 h-4" />
</Button>
<Button size="icon" variant="ghost" className="text-white hover:bg-white/20">
<Edit className="w-4 h-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="text-red-400 hover:bg-red-500/20"
onClick={() => deleteProduct(product.id!)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
<CardContent className="p-3">
<p className="text-white text-sm font-medium truncate">{product.name}</p>
<div className="flex items-center justify-between mt-1">
<span className="text-cyan-400 font-bold">{product.price} </span>
<span className="text-slate-500 text-xs"> {product.sales}</span>
</div>
</CardContent>
</Card>
))}
</div>
)}
</CardContent>
</Card>
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent className="bg-slate-800 border-slate-700">
<DialogHeader>
<DialogTitle className="text-white"></DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={newProduct.name}
onChange={(e) => setNewProduct({ ...newProduct, name: e.target.value })}
placeholder="请输入商品名称"
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Select value={newProduct.category} onValueChange={(v) => setNewProduct({ ...newProduct, category: v })}>
<SelectTrigger className="bg-slate-700/50 border-slate-600 text-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="avatar"></SelectItem>
<SelectItem value="gift"></SelectItem>
<SelectItem value="vip">VIP特权</SelectItem>
<SelectItem value="skin"></SelectItem>
<SelectItem value="points"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
type="number"
value={newProduct.price}
onChange={(e) => setNewProduct({ ...newProduct, price: Number(e.target.value) })}
placeholder="请输入价格"
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={newProduct.description}
onChange={(e) => setNewProduct({ ...newProduct, description: e.target.value })}
placeholder="商品描述"
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div className="flex gap-3 pt-2">
<Button
variant="outline"
onClick={() => setShowAddDialog(false)}
className="flex-1 border-slate-600 text-slate-300"
>
</Button>
<Button onClick={addProduct} className="flex-1 bg-cyan-500 hover:bg-cyan-600">
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

7
app/admin/mall/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
import dynamic from "next/dynamic"
const AdminMallClient = dynamic(() => import("./client"), { ssr: false })
export default function AdminMallPage() {
return <AdminMallClient />
}

233
app/admin/orders/client.tsx Normal file
View File

@@ -0,0 +1,233 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { ArrowLeft, Search, MoreVertical, Eye, RefreshCw, CheckCircle, XCircle, Clock, Package } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { db, type Order } from "@/lib/db/schema"
export default function AdminOrdersClient() {
const router = useRouter()
const [orders, setOrders] = useState<Order[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [loading, setLoading] = useState(true)
useEffect(() => {
loadOrders()
}, [])
const loadOrders = async () => {
setLoading(true)
try {
const allOrders = await db.orders.orderBy("createdAt").reverse().toArray()
setOrders(allOrders)
} catch (error) {
console.error("Failed to load orders:", error)
}
setLoading(false)
}
const filteredOrders = orders.filter((order) => {
const matchesSearch =
order.orderNo?.includes(searchTerm) || order.itemName?.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = statusFilter === "all" || order.status === statusFilter
return matchesSearch && matchesStatus
})
const updateOrderStatus = async (orderId: number, status: string) => {
await db.orders.update(orderId, { status: status as any })
loadOrders()
}
const getStatusBadge = (status: string) => {
switch (status) {
case "completed":
return <Badge className="bg-green-500/20 text-green-400"></Badge>
case "pending":
return <Badge className="bg-amber-500/20 text-amber-400"></Badge>
case "cancelled":
return <Badge className="bg-red-500/20 text-red-400"></Badge>
case "refunded":
return <Badge className="bg-purple-500/20 text-purple-400">退</Badge>
default:
return <Badge className="bg-slate-500/20 text-slate-400">{status}</Badge>
}
}
return (
<div className="min-h-screen bg-slate-900 p-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => router.push("/admin")}
className="text-slate-400 hover:text-white"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="text-2xl font-bold text-white"></h1>
</div>
<Card className="bg-slate-800/50 border-slate-700 mb-6">
<CardContent className="p-4">
<div className="flex flex-wrap gap-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<Input
placeholder="搜索订单号/商品名..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-slate-700/50 border-slate-600 text-white pl-10"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[150px] bg-slate-700/50 border-slate-600 text-white">
<SelectValue placeholder="订单状态" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="completed"></SelectItem>
<SelectItem value="cancelled"></SelectItem>
<SelectItem value="refunded">退</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={loadOrders} className="border-slate-600 text-slate-300 bg-transparent">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">{orders.length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-amber-400">{orders.filter((o) => o.status === "pending").length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-green-400">{orders.filter((o) => o.status === "completed").length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-cyan-400">
{orders.reduce((sum, o) => sum + (o.amount || 0), 0).toLocaleString()}
</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
</div>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"> ({filteredOrders.length})</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8 text-slate-400">...</div>
) : filteredOrders.length === 0 ? (
<div className="text-center py-8 text-slate-400"></div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-700">
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
</tr>
</thead>
<tbody>
{filteredOrders.map((order) => (
<tr key={order.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
<td className="py-3 px-4 text-cyan-400 font-mono text-sm">{order.orderNo}</td>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<Package className="w-4 h-4 text-slate-500" />
<span className="text-white">{order.itemName}</span>
</div>
</td>
<td className="py-3 px-4">
<Badge variant="outline" className="border-slate-600 text-slate-400">
{order.type === "product" ? "商品" : order.type === "service" ? "服务" : "预订"}
</Badge>
</td>
<td className="py-3 px-4 text-amber-400 font-medium">{order.amount} </td>
<td className="py-3 px-4">{getStatusBadge(order.status)}</td>
<td className="py-3 px-4 text-slate-400 text-sm">
{order.createdAt ? new Date(order.createdAt).toLocaleString("zh-CN") : "-"}
</td>
<td className="py-3 px-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="text-slate-400">
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-slate-800 border-slate-700">
<DropdownMenuItem className="text-slate-300">
<Eye className="w-4 h-4 mr-2" />
</DropdownMenuItem>
{order.status === "pending" && (
<>
<DropdownMenuItem
className="text-green-400"
onClick={() => updateOrderStatus(order.id!, "completed")}
>
<CheckCircle className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-400"
onClick={() => updateOrderStatus(order.id!, "cancelled")}
>
<XCircle className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</>
)}
{order.status === "completed" && (
<DropdownMenuItem
className="text-purple-400"
onClick={() => updateOrderStatus(order.id!, "refunded")}
>
<Clock className="w-4 h-4 mr-2" />
退
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,7 @@
import dynamic from "next/dynamic"
const AdminOrdersClient = dynamic(() => import("./client"), { ssr: false })
export default function AdminOrdersPage() {
return <AdminOrdersClient />
}

584
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,584 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import {
LayoutDashboard,
Users,
Radio,
Building2,
Star,
ShoppingBag,
FileText,
Wallet,
MessageSquare,
Settings,
LogOut,
TrendingUp,
UserCheck,
CreditCard,
Gamepad2,
Hotel,
Trophy,
ChevronRight,
Bell,
Search,
Menu,
X,
BarChart3,
PieChart,
Activity,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { db } from "@/lib/db/schema"
// 后台统计数据类型
interface DashboardStats {
totalUsers: number
totalStreamers: number
totalOrders: number
totalRevenue: number
todayNewUsers: number
todayOrders: number
activeStreamers: number
pendingPawns: number
}
// 侧边栏菜单项
const menuItems = [
{ id: "dashboard", label: "数据概览", icon: LayoutDashboard, path: "/admin" },
{ id: "users", label: "用户管理", icon: Users, path: "/admin/users" },
{ id: "streamers", label: "主播管理", icon: Radio, path: "/admin/streamers" },
{ id: "guilds", label: "公会管理", icon: Building2, path: "/admin/guilds" },
{ id: "stars", label: "明星管理", icon: Star, path: "/admin/stars" },
{ id: "live", label: "直播管理", icon: Gamepad2, path: "/admin/live" },
{ id: "party", label: "派对群管理", icon: MessageSquare, path: "/admin/party" },
{ id: "mall", label: "商城管理", icon: ShoppingBag, path: "/admin/mall" },
{ id: "orders", label: "订单管理", icon: FileText, path: "/admin/orders" },
{ id: "pawn", label: "典当管理", icon: Wallet, path: "/admin/pawn" },
{ id: "hotel", label: "酒店/网咖", icon: Hotel, path: "/admin/hotel" },
{ id: "content", label: "内容管理", icon: Trophy, path: "/admin/content" },
{ id: "finance", label: "财务管理", icon: CreditCard, path: "/admin/finance" },
{ id: "settings", label: "系统设置", icon: Settings, path: "/admin/settings" },
]
export default function AdminDashboard() {
const router = useRouter()
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [sidebarOpen, setSidebarOpen] = useState(true)
const [currentMenu, setCurrentMenu] = useState("dashboard")
const [stats, setStats] = useState<DashboardStats>({
totalUsers: 0,
totalStreamers: 0,
totalOrders: 0,
totalRevenue: 0,
todayNewUsers: 0,
todayOrders: 0,
activeStreamers: 0,
pendingPawns: 0,
})
const [recentUsers, setRecentUsers] = useState<any[]>([])
const [recentOrders, setRecentOrders] = useState<any[]>([])
// 加载统计数据
useEffect(() => {
if (isLoggedIn) {
loadDashboardData()
}
}, [isLoggedIn])
const loadDashboardData = async () => {
try {
const users = await db.users.toArray()
const streamers = await db.streamers.toArray()
const orders = await db.orders.toArray()
const pawns = await db.accountPawns.toArray()
const transactions = await db.transactions.toArray()
// 计算总收入
const totalRevenue = transactions.filter((t) => t.type === "recharge").reduce((sum, t) => sum + t.amount, 0)
// 今日数据
const today = new Date()
today.setHours(0, 0, 0, 0)
const todayTimestamp = today.getTime()
const todayNewUsers = users.filter((u) => new Date(u.createdAt).getTime() >= todayTimestamp).length
const todayOrders = orders.filter((o) => new Date(o.createdAt).getTime() >= todayTimestamp).length
setStats({
totalUsers: users.length,
totalStreamers: streamers.length,
totalOrders: orders.length,
totalRevenue,
todayNewUsers,
todayOrders,
activeStreamers: streamers.filter((s) => s.isLive).length,
pendingPawns: pawns.filter((p) => p.status === "pending").length,
})
// 最近用户
setRecentUsers(users.slice(-5).reverse())
// 最近订单
setRecentOrders(orders.slice(-5).reverse())
} catch (error) {
console.error("Failed to load dashboard data:", error)
}
}
// 登录处理
const handleLogin = () => {
if (username === "admin" && password === "admin123") {
setIsLoggedIn(true)
localStorage.setItem("admin_logged_in", "true")
} else {
alert("用户名或密码错误")
}
}
// 检查登录状态
useEffect(() => {
const loggedIn = localStorage.getItem("admin_logged_in")
if (loggedIn === "true") {
setIsLoggedIn(true)
}
}, [])
// 退出登录
const handleLogout = () => {
setIsLoggedIn(false)
localStorage.removeItem("admin_logged_in")
}
// 登录页面
if (!isLoggedIn) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex items-center justify-center p-4">
<Card className="w-full max-w-md bg-slate-800/80 border-slate-700">
<CardHeader className="text-center">
<div className="w-16 h-16 bg-gradient-to-br from-cyan-500 to-purple-600 rounded-2xl mx-auto mb-4 flex items-center justify-center">
<Gamepad2 className="w-8 h-8 text-white" />
</div>
<CardTitle className="text-2xl text-white"></CardTitle>
<p className="text-slate-400 text-sm mt-2"></p>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名"
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
className="bg-slate-700/50 border-slate-600 text-white"
onKeyDown={(e) => e.key === "Enter" && handleLogin()}
/>
</div>
<Button
onClick={handleLogin}
className="w-full bg-gradient-to-r from-cyan-500 to-purple-600 hover:from-cyan-600 hover:to-purple-700"
>
</Button>
<p className="text-center text-slate-500 text-xs">默认账号: admin / admin123</p>
</CardContent>
</Card>
</div>
)
}
// 后台主界面
return (
<div className="min-h-screen bg-slate-900 flex">
{/* 侧边栏 */}
<aside
className={`${sidebarOpen ? "w-64" : "w-20"} bg-slate-800 border-r border-slate-700 transition-all duration-300 flex flex-col`}
>
{/* Logo */}
<div className="h-16 border-b border-slate-700 flex items-center justify-center px-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-cyan-500 to-purple-600 rounded-xl flex items-center justify-center">
<Gamepad2 className="w-5 h-5 text-white" />
</div>
{sidebarOpen && <span className="text-white font-bold"></span>}
</div>
</div>
{/* 菜单 */}
<nav className="flex-1 py-4 overflow-y-auto">
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => {
setCurrentMenu(item.id)
if (item.id !== "dashboard") {
router.push(item.path)
}
}}
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
currentMenu === item.id
? "bg-cyan-500/20 text-cyan-400 border-r-2 border-cyan-400"
: "text-slate-400 hover:bg-slate-700/50 hover:text-white"
}`}
>
<item.icon className="w-5 h-5 shrink-0" />
{sidebarOpen && <span>{item.label}</span>}
</button>
))}
</nav>
{/* 退出登录 */}
<div className="p-4 border-t border-slate-700">
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-3 text-slate-400 hover:bg-red-500/20 hover:text-red-400 rounded-lg transition-colors"
>
<LogOut className="w-5 h-5" />
{sidebarOpen && <span>退</span>}
</button>
</div>
</aside>
{/* 主内容区 */}
<main className="flex-1 flex flex-col">
{/* 顶部栏 */}
<header className="h-16 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-6">
<div className="flex items-center gap-4">
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="text-slate-400 hover:text-white">
{sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<Input placeholder="搜索..." className="w-64 bg-slate-700/50 border-slate-600 text-white pl-10" />
</div>
</div>
<div className="flex items-center gap-4">
<button className="relative text-slate-400 hover:text-white">
<Bell className="w-5 h-5" />
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-xs text-white flex items-center justify-center">
3
</span>
</button>
<div className="flex items-center gap-3">
<Avatar className="w-8 h-8">
<AvatarImage src="/admin-avatar.png" />
<AvatarFallback>AD</AvatarFallback>
</Avatar>
<span className="text-white text-sm"></span>
</div>
</div>
</header>
{/* 仪表盘内容 */}
<div className="flex-1 p-6 overflow-y-auto">
<h1 className="text-2xl font-bold text-white mb-6"></h1>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card className="bg-gradient-to-br from-cyan-500/20 to-cyan-600/10 border-cyan-500/30">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-cyan-400 text-sm"></p>
<p className="text-2xl font-bold text-white">{stats.totalUsers.toLocaleString()}</p>
<p className="text-xs text-green-400">+{stats.todayNewUsers} </p>
</div>
<div className="w-12 h-12 bg-cyan-500/20 rounded-xl flex items-center justify-center">
<Users className="w-6 h-6 text-cyan-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-purple-500/20 to-purple-600/10 border-purple-500/30">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-400 text-sm"></p>
<p className="text-2xl font-bold text-white">{stats.totalStreamers}</p>
<p className="text-xs text-green-400">{stats.activeStreamers} </p>
</div>
<div className="w-12 h-12 bg-purple-500/20 rounded-xl flex items-center justify-center">
<Radio className="w-6 h-6 text-purple-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-amber-500/20 to-amber-600/10 border-amber-500/30">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-amber-400 text-sm"></p>
<p className="text-2xl font-bold text-white">{stats.totalOrders}</p>
<p className="text-xs text-green-400">+{stats.todayOrders} </p>
</div>
<div className="w-12 h-12 bg-amber-500/20 rounded-xl flex items-center justify-center">
<FileText className="w-6 h-6 text-amber-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-500/20 to-green-600/10 border-green-500/30">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-green-400 text-sm"></p>
<p className="text-2xl font-bold text-white">¥{stats.totalRevenue.toLocaleString()}</p>
<p className="text-xs text-amber-400">{stats.pendingPawns} </p>
</div>
<div className="w-12 h-12 bg-green-500/20 rounded-xl flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-400" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 图表区域 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-cyan-400" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-48 flex items-end justify-around gap-2">
{[65, 45, 78, 52, 88, 70, 95].map((height, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<div
className="w-full bg-gradient-to-t from-cyan-500 to-purple-500 rounded-t"
style={{ height: `${height}%` }}
/>
<span className="text-xs text-slate-500">{["一", "二", "三", "四", "五", "六", "日"][i]}</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<PieChart className="w-5 h-5 text-purple-400" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-around">
<div className="relative w-32 h-32">
<svg className="w-full h-full" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="none" stroke="#1e293b" strokeWidth="20" />
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="#06b6d4"
strokeWidth="20"
strokeDasharray="75 175"
strokeDashoffset="0"
/>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="#a855f7"
strokeWidth="20"
strokeDasharray="50 200"
strokeDashoffset="-75"
/>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="#f59e0b"
strokeWidth="20"
strokeDasharray="45 205"
strokeDashoffset="-125"
/>
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke="#22c55e"
strokeWidth="20"
strokeDasharray="30 220"
strokeDashoffset="-170"
/>
</svg>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-cyan-500 rounded-full" />
<span className="text-slate-400 text-sm"> 30%</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-purple-500 rounded-full" />
<span className="text-slate-400 text-sm"> 20%</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-amber-500 rounded-full" />
<span className="text-slate-400 text-sm"> 18%</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500 rounded-full" />
<span className="text-slate-400 text-sm"> 12%</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 快捷操作和最近数据 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 快捷操作 */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Button
onClick={() => router.push("/admin/users")}
variant="outline"
className="w-full justify-between border-slate-600 text-slate-300 hover:bg-slate-700"
>
<span className="flex items-center gap-2">
<UserCheck className="w-4 h-4" />
</span>
<Badge className="bg-cyan-500/20 text-cyan-400">{stats.todayNewUsers}</Badge>
</Button>
<Button
onClick={() => router.push("/admin/pawn")}
variant="outline"
className="w-full justify-between border-slate-600 text-slate-300 hover:bg-slate-700"
>
<span className="flex items-center gap-2">
<Wallet className="w-4 h-4" />
</span>
<Badge className="bg-amber-500/20 text-amber-400">{stats.pendingPawns}</Badge>
</Button>
<Button
onClick={() => router.push("/admin/streamers")}
variant="outline"
className="w-full justify-between border-slate-600 text-slate-300 hover:bg-slate-700"
>
<span className="flex items-center gap-2">
<Radio className="w-4 h-4" />
</span>
<Badge className="bg-purple-500/20 text-purple-400">2</Badge>
</Button>
<Button
onClick={() => router.push("/admin/orders")}
variant="outline"
className="w-full justify-between border-slate-600 text-slate-300 hover:bg-slate-700"
>
<span className="flex items-center gap-2">
<FileText className="w-4 h-4" />
</span>
<ChevronRight className="w-4 h-4" />
</Button>
</CardContent>
</Card>
{/* 最近用户 */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{recentUsers.length > 0 ? (
recentUsers.map((user, i) => (
<div key={i} className="flex items-center gap-3">
<Avatar className="w-8 h-8">
<AvatarImage src={user.avatar || "/placeholder.svg"} />
<AvatarFallback>{user.nickname?.[0]}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-white text-sm truncate">{user.nickname}</p>
<p className="text-slate-500 text-xs">{user.phone || "未绑定手机"}</p>
</div>
<Badge className="bg-green-500/20 text-green-400 text-xs"></Badge>
</div>
))
) : (
<p className="text-slate-500 text-sm text-center py-4"></p>
)}
</CardContent>
</Card>
{/* 系统状态 */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Activity className="w-5 h-5 text-green-400" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-400"></span>
<span className="text-green-400"></span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div className="h-full bg-green-500 rounded-full" style={{ width: "25%" }} />
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-400">API服务</span>
<span className="text-green-400"></span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div className="h-full bg-green-500 rounded-full" style={{ width: "15%" }} />
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-400"></span>
<span className="text-cyan-400">48%</span>
</div>
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div className="h-full bg-cyan-500 rounded-full" style={{ width: "48%" }} />
</div>
</div>
<div className="pt-2 border-t border-slate-700">
<p className="text-slate-500 text-xs">: {new Date().toLocaleString("zh-CN")}</p>
</div>
</CardContent>
</Card>
</div>
</div>
</main>
</div>
)
}

254
app/admin/pawn/client.tsx Normal file
View File

@@ -0,0 +1,254 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { ArrowLeft, Search, MoreVertical, Eye, RefreshCw, CheckCircle, XCircle, Wallet, FileText } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { db, type AccountPawn } from "@/lib/db/schema"
export default function AdminPawnClient() {
const router = useRouter()
const [pawns, setPawns] = useState<AccountPawn[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [loading, setLoading] = useState(true)
useEffect(() => {
loadPawns()
}, [])
const loadPawns = async () => {
setLoading(true)
try {
const allPawns = await db.accountPawns.orderBy("createdAt").reverse().toArray()
setPawns(allPawns)
} catch (error) {
console.error("Failed to load pawns:", error)
}
setLoading(false)
}
const filteredPawns = pawns.filter((pawn) => {
const matchesSearch =
pawn.game?.toLowerCase().includes(searchTerm.toLowerCase()) || pawn.contactPhone?.includes(searchTerm)
const matchesStatus = statusFilter === "all" || pawn.status === statusFilter
return matchesSearch && matchesStatus
})
const updatePawnStatus = async (pawnId: number, status: string) => {
await db.accountPawns.update(pawnId, { status: status as any })
loadPawns()
}
const getStatusBadge = (status: string) => {
switch (status) {
case "pending":
return <Badge className="bg-amber-500/20 text-amber-400"></Badge>
case "approved":
return <Badge className="bg-cyan-500/20 text-cyan-400"></Badge>
case "pawned":
return <Badge className="bg-purple-500/20 text-purple-400"></Badge>
case "redeemed":
return <Badge className="bg-green-500/20 text-green-400"></Badge>
case "expired":
return <Badge className="bg-red-500/20 text-red-400"></Badge>
case "rejected":
return <Badge className="bg-slate-500/20 text-slate-400"></Badge>
default:
return <Badge className="bg-slate-500/20 text-slate-400">{status}</Badge>
}
}
return (
<div className="min-h-screen bg-slate-900 p-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => router.push("/admin")}
className="text-slate-400 hover:text-white"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="text-2xl font-bold text-white"></h1>
</div>
<Card className="bg-slate-800/50 border-slate-700 mb-6">
<CardContent className="p-4">
<div className="flex flex-wrap gap-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<Input
placeholder="搜索游戏/手机号..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-slate-700/50 border-slate-600 text-white pl-10"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[150px] bg-slate-700/50 border-slate-600 text-white">
<SelectValue placeholder="典当状态" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="approved"></SelectItem>
<SelectItem value="pawned"></SelectItem>
<SelectItem value="redeemed"></SelectItem>
<SelectItem value="expired"></SelectItem>
<SelectItem value="rejected"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={loadPawns} className="border-slate-600 text-slate-300 bg-transparent">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="bg-gradient-to-br from-amber-500/20 to-amber-600/10 border-amber-500/30">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">{pawns.length}</p>
<p className="text-amber-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-cyan-500/20 to-cyan-600/10 border-cyan-500/30">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">{pawns.filter((p) => p.status === "pending").length}</p>
<p className="text-cyan-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-purple-500/20 to-purple-600/10 border-purple-500/30">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">{pawns.filter((p) => p.status === "pawned").length}</p>
<p className="text-purple-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-500/20 to-green-600/10 border-green-500/30">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">
¥
{pawns
.filter((p) => p.status === "pawned")
.reduce((sum, p) => sum + (p.pawnAmount || 0), 0)
.toLocaleString()}
</p>
<p className="text-green-400 text-sm"></p>
</CardContent>
</Card>
</div>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"> ({filteredPawns.length})</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8 text-slate-400">...</div>
) : filteredPawns.length === 0 ? (
<div className="text-center py-8 text-slate-400"></div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-700">
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
</tr>
</thead>
<tbody>
{filteredPawns.map((pawn) => (
<tr key={pawn.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<Wallet className="w-4 h-4 text-amber-500" />
<span className="text-white">{pawn.game}</span>
</div>
</td>
<td className="py-3 px-4 text-cyan-400">¥{pawn.estimatedValue?.toLocaleString()}</td>
<td className="py-3 px-4 text-amber-400 font-medium">¥{pawn.pawnAmount?.toLocaleString()}</td>
<td className="py-3 px-4 text-slate-300">{pawn.pawnPeriod}</td>
<td className="py-3 px-4 text-slate-300">{pawn.contactPhone}</td>
<td className="py-3 px-4">{getStatusBadge(pawn.status)}</td>
<td className="py-3 px-4 text-slate-400 text-sm">
{pawn.createdAt ? new Date(pawn.createdAt).toLocaleString("zh-CN") : "-"}
</td>
<td className="py-3 px-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="text-slate-400">
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-slate-800 border-slate-700">
<DropdownMenuItem className="text-slate-300">
<Eye className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300">
<FileText className="w-4 h-4 mr-2" />
</DropdownMenuItem>
{pawn.status === "pending" && (
<>
<DropdownMenuItem
className="text-green-400"
onClick={() => updatePawnStatus(pawn.id!, "approved")}
>
<CheckCircle className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-400"
onClick={() => updatePawnStatus(pawn.id!, "rejected")}
>
<XCircle className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</>
)}
{pawn.status === "approved" && (
<DropdownMenuItem
className="text-purple-400"
onClick={() => updatePawnStatus(pawn.id!, "pawned")}
>
<Wallet className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
{pawn.status === "pawned" && (
<DropdownMenuItem
className="text-green-400"
onClick={() => updatePawnStatus(pawn.id!, "redeemed")}
>
<CheckCircle className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

7
app/admin/pawn/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
import dynamic from "next/dynamic"
const AdminPawnClient = dynamic(() => import("./client"), { ssr: false })
export default function AdminPawnPage() {
return <AdminPawnClient />
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

300
app/admin/settings/page.tsx Normal file
View File

@@ -0,0 +1,300 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ArrowLeft, Save, Settings, Shield, Globe, CreditCard } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function AdminSettingsPage() {
const router = useRouter()
const [settings, setSettings] = useState({
siteName: "玩值电竞",
siteDescription: "专业游戏电竞社交平台",
contactEmail: "support@wanzhi.com",
contactPhone: "400-888-8888",
enableRegistration: true,
enablePawn: true,
enableLive: true,
enableMall: true,
maintenanceMode: false,
minPawnAmount: 5000,
maxPawnPeriod: 30,
pawnFeeRate: 0.05,
rechargeBonus: 0.1,
vipDiscount: 0.9,
})
const handleSave = () => {
localStorage.setItem("admin_settings", JSON.stringify(settings))
alert("设置已保存")
}
return (
<div className="min-h-screen bg-slate-900 p-6">
{/* 顶部导航 */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => router.push("/admin")}
className="text-slate-400 hover:text-white"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="text-2xl font-bold text-white"></h1>
</div>
<Button onClick={handleSave} className="bg-cyan-500 hover:bg-cyan-600">
<Save className="w-4 h-4 mr-2" />
</Button>
</div>
<Tabs defaultValue="general" className="space-y-6">
<TabsList className="bg-slate-800 border-slate-700">
<TabsTrigger value="general" className="data-[state=active]:bg-slate-700">
<Settings className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="features" className="data-[state=active]:bg-slate-700">
<Shield className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="pawn" className="data-[state=active]:bg-slate-700">
<CreditCard className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="payment" className="data-[state=active]:bg-slate-700">
<Globe className="w-4 h-4 mr-2" />
</TabsTrigger>
</TabsList>
<TabsContent value="general">
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
<CardDescription className="text-slate-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={settings.siteName}
onChange={(e) => setSettings({ ...settings, siteName: e.target.value })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={settings.siteDescription}
onChange={(e) => setSettings({ ...settings, siteDescription: e.target.value })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={settings.contactEmail}
onChange={(e) => setSettings({ ...settings, contactEmail: e.target.value })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={settings.contactPhone}
onChange={(e) => setSettings({ ...settings, contactPhone: e.target.value })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="features">
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
<CardDescription className="text-slate-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium"></p>
<p className="text-slate-400 text-sm"></p>
</div>
<Switch
checked={settings.enableRegistration}
onCheckedChange={(v) => setSettings({ ...settings, enableRegistration: v })}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium"></p>
<p className="text-slate-400 text-sm"></p>
</div>
<Switch
checked={settings.enablePawn}
onCheckedChange={(v) => setSettings({ ...settings, enablePawn: v })}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium"></p>
<p className="text-slate-400 text-sm"></p>
</div>
<Switch
checked={settings.enableLive}
onCheckedChange={(v) => setSettings({ ...settings, enableLive: v })}
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium"></p>
<p className="text-slate-400 text-sm"></p>
</div>
<Switch
checked={settings.enableMall}
onCheckedChange={(v) => setSettings({ ...settings, enableMall: v })}
/>
</div>
<div className="flex items-center justify-between border-t border-slate-700 pt-6">
<div>
<p className="text-red-400 font-medium"></p>
<p className="text-slate-400 text-sm">访</p>
</div>
<Switch
checked={settings.maintenanceMode}
onCheckedChange={(v) => setSettings({ ...settings, maintenanceMode: v })}
/>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="pawn">
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
<CardDescription className="text-slate-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-400 mb-1 block"> ()</label>
<Input
type="number"
value={settings.minPawnAmount}
onChange={(e) => setSettings({ ...settings, minPawnAmount: Number(e.target.value) })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"> ()</label>
<Input
type="number"
value={settings.maxPawnPeriod}
onChange={(e) => setSettings({ ...settings, maxPawnPeriod: Number(e.target.value) })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
type="number"
step="0.01"
value={settings.pawnFeeRate}
onChange={(e) => setSettings({ ...settings, pawnFeeRate: Number(e.target.value) })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
<p className="text-slate-500 text-xs mt-1">: {(settings.pawnFeeRate * 100).toFixed(1)}%</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="payment">
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"></CardTitle>
<CardDescription className="text-slate-400"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
type="number"
step="0.01"
value={settings.rechargeBonus}
onChange={(e) => setSettings({ ...settings, rechargeBonus: Number(e.target.value) })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
<p className="text-slate-500 text-xs mt-1"> {(settings.rechargeBonus * 100).toFixed(0)}%</p>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block">VIP折扣</label>
<Input
type="number"
step="0.01"
value={settings.vipDiscount}
onChange={(e) => setSettings({ ...settings, vipDiscount: Number(e.target.value) })}
className="bg-slate-700/50 border-slate-600 text-white"
/>
<p className="text-slate-500 text-xs mt-1">VIP享 {(settings.vipDiscount * 10).toFixed(1)} </p>
</div>
</div>
<div className="border-t border-slate-700 pt-4">
<h4 className="text-white font-medium mb-3"></h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="bg-slate-700/50 border-slate-600 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-500/20 rounded-lg flex items-center justify-center">
<CreditCard className="w-5 h-5 text-green-400" />
</div>
<div>
<p className="text-white font-medium"></p>
<p className="text-green-400 text-xs"></p>
</div>
</div>
</Card>
<Card className="bg-slate-700/50 border-slate-600 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-500/20 rounded-lg flex items-center justify-center">
<CreditCard className="w-5 h-5 text-blue-400" />
</div>
<div>
<p className="text-white font-medium"></p>
<p className="text-blue-400 text-xs"></p>
</div>
</div>
</Card>
<Card className="bg-slate-700/50 border-slate-600 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-500/20 rounded-lg flex items-center justify-center">
<CreditCard className="w-5 h-5 text-amber-400" />
</div>
<div>
<p className="text-white font-medium"></p>
<p className="text-amber-400 text-xs"></p>
</div>
</div>
</Card>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,313 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import {
ArrowLeft,
Search,
Plus,
MoreVertical,
Radio,
Eye,
Edit,
Trash2,
CheckCircle,
XCircle,
RefreshCw,
Users,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { db, type Streamer } from "@/lib/db/schema"
export default function AdminStreamersClient() {
const router = useRouter()
const [streamers, setStreamers] = useState<Streamer[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [loading, setLoading] = useState(true)
const [showAddDialog, setShowAddDialog] = useState(false)
const [newStreamer, setNewStreamer] = useState({ name: "", game: "", description: "" })
useEffect(() => {
loadStreamers()
}, [])
const loadStreamers = async () => {
setLoading(true)
try {
const allStreamers = await db.streamers.toArray()
setStreamers(allStreamers)
} catch (error) {
console.error("Failed to load streamers:", error)
}
setLoading(false)
}
const filteredStreamers = streamers.filter(
(streamer) =>
streamer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
streamer.game?.toLowerCase().includes(searchTerm.toLowerCase()),
)
const toggleLiveStatus = async (streamerId: number, isLive: boolean) => {
await db.streamers.update(streamerId, { isLive: !isLive })
loadStreamers()
}
const deleteStreamer = async (streamerId: number) => {
if (confirm("确定要删除该主播吗?")) {
await db.streamers.delete(streamerId)
loadStreamers()
}
}
const addStreamer = async () => {
if (!newStreamer.name || !newStreamer.game) {
alert("请填写主播名称和游戏类型")
return
}
await db.streamers.add({
name: newStreamer.name,
game: newStreamer.game,
description: newStreamer.description,
avatar: `/placeholder.svg?height=100&width=100&query=${newStreamer.name} avatar`,
cover: `/placeholder.svg?height=200&width=300&query=${newStreamer.game} gaming`,
followers: 0,
isLive: false,
isVerified: false,
tags: [newStreamer.game],
guildId: undefined,
createdAt: new Date(),
})
setShowAddDialog(false)
setNewStreamer({ name: "", game: "", description: "" })
loadStreamers()
}
return (
<div className="min-h-screen bg-slate-900 p-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => router.push("/admin")}
className="text-slate-400 hover:text-white"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="text-2xl font-bold text-white"></h1>
</div>
<Card className="bg-slate-800/50 border-slate-700 mb-6">
<CardContent className="p-4">
<div className="flex flex-wrap gap-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<Input
placeholder="搜索主播名称/游戏..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-slate-700/50 border-slate-600 text-white pl-10"
/>
</div>
<Button
variant="outline"
onClick={loadStreamers}
className="border-slate-600 text-slate-300 bg-transparent"
>
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-purple-500 hover:bg-purple-600" onClick={() => setShowAddDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">{streamers.length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-green-400">{streamers.filter((s) => s.isLive).length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-cyan-400">{streamers.filter((s) => s.isVerified).length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-amber-400">
{streamers.reduce((sum, s) => sum + (s.followers || 0), 0).toLocaleString()}
</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
</div>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"> ({filteredStreamers.length})</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8 text-slate-400">...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredStreamers.map((streamer) => (
<Card key={streamer.id} className="bg-slate-700/50 border-slate-600 overflow-hidden">
<div className="relative h-32">
<img
src={streamer.cover || "/placeholder.svg?height=128&width=300&query=gaming cover"}
alt={streamer.name}
className="w-full h-full object-cover"
/>
{streamer.isLive && (
<Badge className="absolute top-2 left-2 bg-red-500">
<Radio className="w-3 h-3 mr-1 animate-pulse" />
</Badge>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 bg-black/50 text-white hover:bg-black/70"
>
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-slate-800 border-slate-700">
<DropdownMenuItem className="text-slate-300">
<Eye className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300">
<Edit className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem
className="text-slate-300"
onClick={() => toggleLiveStatus(streamer.id!, streamer.isLive || false)}
>
{streamer.isLive ? (
<>
<XCircle className="w-4 h-4 mr-2" />
</>
) : (
<>
<CheckCircle className="w-4 h-4 mr-2" />
</>
)}
</DropdownMenuItem>
<DropdownMenuItem className="text-red-400" onClick={() => deleteStreamer(streamer.id!)}>
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<CardContent className="p-4">
<div className="flex items-center gap-3 mb-2">
<Avatar className="w-12 h-12 border-2 border-slate-600">
<AvatarImage src={streamer.avatar || "/placeholder.svg"} />
<AvatarFallback>{streamer.name[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-white font-medium">{streamer.name}</span>
{streamer.isVerified && <CheckCircle className="w-4 h-4 text-cyan-400" />}
</div>
<p className="text-slate-400 text-sm">{streamer.game}</p>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1 text-slate-400">
<Users className="w-4 h-4" />
<span>{(streamer.followers || 0).toLocaleString()}</span>
</div>
<div className="flex gap-1">
{streamer.tags?.slice(0, 2).map((tag, i) => (
<Badge key={i} variant="outline" className="text-xs border-slate-600 text-slate-400">
{tag}
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</CardContent>
</Card>
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent className="bg-slate-800 border-slate-700">
<DialogHeader>
<DialogTitle className="text-white"></DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={newStreamer.name}
onChange={(e) => setNewStreamer({ ...newStreamer, name: e.target.value })}
placeholder="请输入主播名称"
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={newStreamer.game}
onChange={(e) => setNewStreamer({ ...newStreamer, game: e.target.value })}
placeholder="如:王者荣耀、英雄联盟"
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div>
<label className="text-sm text-slate-400 mb-1 block"></label>
<Input
value={newStreamer.description}
onChange={(e) => setNewStreamer({ ...newStreamer, description: e.target.value })}
placeholder="主播简介"
className="bg-slate-700/50 border-slate-600 text-white"
/>
</div>
<div className="flex gap-3 pt-2">
<Button
variant="outline"
onClick={() => setShowAddDialog(false)}
className="flex-1 border-slate-600 text-slate-300"
>
</Button>
<Button onClick={addStreamer} className="flex-1 bg-purple-500 hover:bg-purple-600">
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,7 @@
import dynamic from "next/dynamic"
const AdminStreamersClient = dynamic(() => import("./client"), { ssr: false })
export default function AdminStreamersPage() {
return <AdminStreamersClient />
}

250
app/admin/users/client.tsx Normal file
View File

@@ -0,0 +1,250 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { ArrowLeft, Search, MoreVertical, UserCheck, UserX, Edit, Trash2, Eye, Plus, RefreshCw } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { db, type User } from "@/lib/db/schema"
export default function AdminUsersClient() {
const router = useRouter()
const [users, setUsers] = useState<User[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [loading, setLoading] = useState(true)
useEffect(() => {
loadUsers()
}, [])
const loadUsers = async () => {
setLoading(true)
try {
const allUsers = await db.users.toArray()
setUsers(allUsers)
} catch (error) {
console.error("Failed to load users:", error)
}
setLoading(false)
}
const filteredUsers = users.filter((user) => {
const matchesSearch =
user.nickname?.toLowerCase().includes(searchTerm.toLowerCase()) || user.phone?.includes(searchTerm)
const matchesStatus = statusFilter === "all" || user.status === statusFilter
return matchesSearch && matchesStatus
})
const updateUserStatus = async (userId: number, status: string) => {
await db.users.update(userId, { status })
loadUsers()
}
const deleteUser = async (userId: number) => {
if (confirm("确定要删除该用户吗?")) {
await db.users.delete(userId)
loadUsers()
}
}
return (
<div className="min-h-screen bg-slate-900 p-6">
<div className="flex items-center gap-4 mb-6">
<Button
variant="ghost"
size="icon"
onClick={() => router.push("/admin")}
className="text-slate-400 hover:text-white"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="text-2xl font-bold text-white"></h1>
</div>
<Card className="bg-slate-800/50 border-slate-700 mb-6">
<CardContent className="p-4">
<div className="flex flex-wrap gap-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
<Input
placeholder="搜索用户名/手机号..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-slate-700/50 border-slate-600 text-white pl-10"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[150px] bg-slate-700/50 border-slate-600 text-white">
<SelectValue placeholder="用户状态" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="all"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="banned"></SelectItem>
<SelectItem value="pending"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={loadUsers} className="border-slate-600 text-slate-300 bg-transparent">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
<Button className="bg-cyan-500 hover:bg-cyan-600">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-white">{users.length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-green-400">{users.filter((u) => u.status === "active").length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-amber-400">
{users.filter((u) => u.vipLevel && u.vipLevel > 0).length}
</p>
<p className="text-slate-400 text-sm">VIP用户</p>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardContent className="p-4 text-center">
<p className="text-2xl font-bold text-red-400">{users.filter((u) => u.status === "banned").length}</p>
<p className="text-slate-400 text-sm"></p>
</CardContent>
</Card>
</div>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white"> ({filteredUsers.length})</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="text-center py-8 text-slate-400">...</div>
) : filteredUsers.length === 0 ? (
<div className="text-center py-8 text-slate-400"></div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-700">
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4">VIP</th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
<th className="text-left text-slate-400 text-sm font-medium py-3 px-4"></th>
</tr>
</thead>
<tbody>
{filteredUsers.map((user) => (
<tr key={user.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
<td className="py-3 px-4">
<div className="flex items-center gap-3">
<Avatar className="w-10 h-10">
<AvatarImage src={user.avatar || "/placeholder.svg"} />
<AvatarFallback>{user.nickname?.[0]}</AvatarFallback>
</Avatar>
<div>
<p className="text-white font-medium">{user.nickname}</p>
<p className="text-slate-500 text-xs">ID: {user.id}</p>
</div>
</div>
</td>
<td className="py-3 px-4 text-slate-300">{user.phone || "未绑定"}</td>
<td className="py-3 px-4 text-amber-400">{user.balance?.toLocaleString() || 0} </td>
<td className="py-3 px-4">
{user.vipLevel && user.vipLevel > 0 ? (
<Badge className="bg-amber-500/20 text-amber-400">VIP{user.vipLevel}</Badge>
) : (
<span className="text-slate-500"></span>
)}
</td>
<td className="py-3 px-4">
<Badge
className={
user.status === "active"
? "bg-green-500/20 text-green-400"
: user.status === "banned"
? "bg-red-500/20 text-red-400"
: "bg-amber-500/20 text-amber-400"
}
>
{user.status === "active" ? "正常" : user.status === "banned" ? "封禁" : "待审"}
</Badge>
</td>
<td className="py-3 px-4 text-slate-400 text-sm">
{user.createdAt ? new Date(user.createdAt).toLocaleDateString("zh-CN") : "-"}
</td>
<td className="py-3 px-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="text-slate-400">
<MoreVertical className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-slate-800 border-slate-700">
<DropdownMenuItem className="text-slate-300 hover:text-white">
<Eye className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem className="text-slate-300 hover:text-white">
<Edit className="w-4 h-4 mr-2" />
</DropdownMenuItem>
{user.status === "active" ? (
<DropdownMenuItem
className="text-red-400 hover:text-red-300"
onClick={() => updateUserStatus(user.id!, "banned")}
>
<UserX className="w-4 h-4 mr-2" />
</DropdownMenuItem>
) : (
<DropdownMenuItem
className="text-green-400 hover:text-green-300"
onClick={() => updateUserStatus(user.id!, "active")}
>
<UserCheck className="w-4 h-4 mr-2" />
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-red-400 hover:text-red-300"
onClick={() => deleteUser(user.id!)}
>
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return null
}

7
app/admin/users/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
import dynamic from "next/dynamic"
const AdminUsersClient = dynamic(() => import("./client"), { ssr: false })
export default function AdminUsersPage() {
return <AdminUsersClient />
}

View File

@@ -0,0 +1,144 @@
"use client"
import { useState } from "react"
import { ArrowLeft, MessageCircle, Phone, Mail, Clock, ChevronRight, Send } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useRouter } from "next/navigation"
export default function CustomerServicePage() {
const router = useRouter()
const [message, setMessage] = useState("")
const [messages, setMessages] = useState([
{ id: 1, type: "system", content: "您好!欢迎使用玩值电竞客服中心,请问有什么可以帮助您的?", time: "10:00" },
])
const faqList = [
{ q: "如何充值玩值币?", a: "点击首页右上角钱包图标,选择充值金额即可" },
{ q: "如何申请退款?", a: "进入订单详情页,点击申请退款按钮" },
{ q: "账号典当如何操作?", a: "进入商城-账号典当,填写账号信息提交评估" },
{ q: "如何联系主播?", a: "在主播主页点击私信按钮即可发送消息" },
]
const sendMessage = () => {
if (!message.trim()) return
const newMsg = {
id: messages.length + 1,
type: "user",
content: message,
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
}
setMessages([...messages, newMsg])
setMessage("")
// 模拟客服回复
setTimeout(() => {
const reply = {
id: messages.length + 2,
type: "system",
content: "感谢您的咨询,客服正在为您处理中,请稍候...",
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
}
setMessages((prev) => [...prev, reply])
}, 1000)
}
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Header */}
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur-sm border-b border-border px-4 py-3 flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-foreground/10 rounded-full transition-colors">
<ArrowLeft size={20} />
</button>
<h1 className="font-bold text-lg"></h1>
</header>
{/* Quick Actions */}
<div className="p-4 grid grid-cols-3 gap-3">
<a href="tel:400-888-8888" className="bg-card rounded-xl p-4 border border-border text-center">
<Phone size={24} className="mx-auto mb-2 text-primary" />
<span className="text-xs text-foreground/70"></span>
</a>
<button className="bg-card rounded-xl p-4 border border-border text-center">
<MessageCircle size={24} className="mx-auto mb-2 text-green-500" />
<span className="text-xs text-foreground/70">线</span>
</button>
<a href="mailto:support@wanzhi.com" className="bg-card rounded-xl p-4 border border-border text-center">
<Mail size={24} className="mx-auto mb-2 text-blue-500" />
<span className="text-xs text-foreground/70"></span>
</a>
</div>
{/* FAQ Section */}
<div className="px-4 mb-4">
<h3 className="text-sm font-bold mb-3 flex items-center gap-2">
<Clock size={14} className="text-primary" />
</h3>
<div className="bg-card rounded-xl border border-border overflow-hidden">
{faqList.map((faq, i) => (
<button
key={i}
className="w-full flex items-center justify-between p-4 border-b border-border last:border-0 text-left"
onClick={() => {
setMessages((prev) => [
...prev,
{
id: prev.length + 1,
type: "user",
content: faq.q,
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
},
{
id: prev.length + 2,
type: "system",
content: faq.a,
time: new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
},
])
}}
>
<span className="text-sm text-foreground/80">{faq.q}</span>
<ChevronRight size={16} className="text-foreground/30" />
</button>
))}
</div>
</div>
{/* Chat Area */}
<div className="flex-1 px-4 space-y-3 overflow-y-auto pb-20">
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.type === "user" ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
msg.type === "user" ? "bg-primary text-white" : "bg-card border border-border"
}`}
>
<p className="text-sm">{msg.content}</p>
<p className={`text-[10px] mt-1 ${msg.type === "user" ? "text-white/60" : "text-foreground/40"}`}>
{msg.time}
</p>
</div>
</div>
))}
</div>
{/* Input Area */}
<div className="fixed bottom-0 left-0 right-0 bg-background border-t border-border p-4">
<div className="flex gap-2">
<Input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="请输入您的问题..."
className="flex-1 bg-foreground/5 border-foreground/10"
onKeyPress={(e) => e.key === "Enter" && sendMessage()}
/>
<Button onClick={sendMessage} className="bg-primary">
<Send size={18} />
</Button>
</div>
</div>
</div>
)
}

View File

@@ -1,34 +1,40 @@
import type { Metadata } from "next";
import { Inter } from 'next/font/google';
import "./globals.css";
import { MobileNav } from "@/components/layout/mobile-nav";
import { AppProvider } from "@/components/providers/app-provider";
import { Toaster } from "@/components/ui/toaster";
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { MobileNav } from "@/components/layout/mobile-nav"
import { AppProvider } from "@/components/providers/app-provider"
import { DatabaseProvider } from "@/components/providers/database-provider"
import { Toaster } from "@/components/ui/toaster"
const inter = Inter({ subsets: ["latin"] });
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "玩值电竞 - WanZhi Esports",
description: "专注打造比体育明星价值高10倍的电竞俱乐部",
generator: 'v0.app'
};
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {
return (
<html lang="zh-CN" className="dark">
<body className={`${inter.className} min-h-screen bg-background text-foreground antialiased overflow-x-hidden pb-20`}>
<AppProvider>
<main className="mx-auto max-w-md min-h-screen bg-background relative shadow-2xl overflow-hidden">
{children}
<MobileNav />
<Toaster />
</main>
</AppProvider>
<body
className={`${inter.className} min-h-screen bg-background text-foreground antialiased overflow-x-hidden pb-20`}
>
<DatabaseProvider>
<AppProvider>
<main className="mx-auto max-w-md min-h-screen bg-background relative shadow-2xl overflow-hidden">
{children}
<MobileNav />
<Toaster />
</main>
</AppProvider>
</DatabaseProvider>
</body>
</html>
);
)
}

160
app/login/page.tsx Normal file
View File

@@ -0,0 +1,160 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Phone, Lock, Eye, EyeOff, MessageSquare } from "lucide-react"
import Image from "next/image"
import { useAppContext } from "@/components/providers/app-provider"
export default function LoginPage() {
const router = useRouter()
const { login } = useAppContext()
const [phone, setPhone] = useState("")
const [code, setCode] = useState("")
const [showPassword, setShowPassword] = useState(false)
const [loginType, setLoginType] = useState<"code" | "password">("code")
const [countdown, setCountdown] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [agreed, setAgreed] = useState(false)
const sendCode = () => {
if (phone.length !== 11) return
setCountdown(60)
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer)
return 0
}
return prev - 1
})
}, 1000)
}
const handleLogin = async () => {
if (!agreed) return
if (phone.length !== 11) return
setIsLoading(true)
const success = await login(phone, code)
setIsLoading(false)
if (success) {
router.push("/")
}
}
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Logo Area */}
<div className="flex-1 flex flex-col items-center justify-center px-8 pt-20">
<div className="w-24 h-24 rounded-2xl bg-gradient-to-br from-primary to-cyan-500 flex items-center justify-center mb-4 shadow-lg shadow-primary/30">
<Image src="/wanzhi-logo.jpg" alt="玩值电竞" width={64} height={64} className="object-contain" />
</div>
<h1 className="text-2xl font-bold text-foreground mb-2"></h1>
<p className="text-sm text-foreground/50"> · · </p>
</div>
{/* Login Form */}
<div className="px-6 pb-12 space-y-4">
{/* Phone Input */}
<div className="relative">
<Phone size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-foreground/40" />
<Input
type="tel"
placeholder="请输入手机号"
value={phone}
onChange={(e) => setPhone(e.target.value.replace(/\D/g, "").slice(0, 11))}
className="pl-12 h-12 bg-foreground/5 border-foreground/10 rounded-xl"
/>
</div>
{/* Code/Password Input */}
<div className="relative">
{loginType === "code" ? (
<MessageSquare size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-foreground/40" />
) : (
<Lock size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-foreground/40" />
)}
<Input
type={loginType === "password" && !showPassword ? "password" : "text"}
placeholder={loginType === "code" ? "请输入验证码" : "请输入密码"}
value={code}
onChange={(e) => setCode(e.target.value)}
className="pl-12 pr-24 h-12 bg-foreground/5 border-foreground/10 rounded-xl"
/>
{loginType === "code" ? (
<button
onClick={sendCode}
disabled={countdown > 0 || phone.length !== 11}
className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-primary disabled:text-foreground/30"
>
{countdown > 0 ? `${countdown}s` : "获取验证码"}
</button>
) : (
<button
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground/40"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
)}
</div>
{/* Switch Login Type */}
<div className="flex justify-between items-center text-sm">
<button onClick={() => setLoginType(loginType === "code" ? "password" : "code")} className="text-primary">
{loginType === "code" ? "密码登录" : "验证码登录"}
</button>
<button className="text-foreground/50">?</button>
</div>
{/* Agreement */}
<div className="flex items-start gap-2 text-xs text-foreground/50">
<button
onClick={() => setAgreed(!agreed)}
className={`w-4 h-4 rounded border flex-shrink-0 mt-0.5 flex items-center justify-center transition-colors ${
agreed ? "bg-primary border-primary" : "border-foreground/30"
}`}
>
{agreed && <span className="text-white text-[10px]"></span>}
</button>
<span>
<span className="text-primary"></span><span className="text-primary"></span>
</span>
</div>
{/* Login Button */}
<Button
onClick={handleLogin}
disabled={!agreed || phone.length !== 11 || isLoading}
className="w-full h-12 rounded-xl bg-gradient-to-r from-primary to-cyan-500 text-white font-bold text-base"
>
{isLoading ? "登录中..." : "登录 / 注册"}
</Button>
{/* Third Party Login */}
<div className="pt-6">
<div className="flex items-center gap-4 mb-6">
<div className="flex-1 h-px bg-foreground/10"></div>
<span className="text-xs text-foreground/30"></span>
<div className="flex-1 h-px bg-foreground/10"></div>
</div>
<div className="flex justify-center gap-8">
{["微信", "QQ", "微博"].map((name) => (
<button
key={name}
className="w-12 h-12 rounded-full bg-foreground/5 flex items-center justify-center text-foreground/50 text-xs"
>
{name}
</button>
))}
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,12 +1,12 @@
"use client"
import { Button } from "@/components/ui/button"
import { ArrowLeft, ShoppingBag, Clock, CheckCircle2, XCircle } from "lucide-react"
import { ArrowLeft, ShoppingBag, Clock, CheckCircle2, XCircle, RotateCcw } from "lucide-react"
import { useRouter } from "next/navigation"
import Image from "next/image"
import { useAppContext } from "@/components/providers/app-provider"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import Link from "next/link"
export default function OrdersPage() {
const router = useRouter()
@@ -20,8 +20,10 @@ export default function OrdersPage() {
return "text-yellow-500"
case "cancelled":
return "text-red-500"
case "refunded":
return "text-blue-500"
default:
return "text-white/50"
return "text-foreground/50"
}
}
@@ -33,6 +35,8 @@ export default function OrdersPage() {
return <Clock size={14} />
case "cancelled":
return <XCircle size={14} />
case "refunded":
return <RotateCcw size={14} />
default:
return null
}
@@ -46,15 +50,40 @@ export default function OrdersPage() {
return "进行中"
case "cancelled":
return "已取消"
case "refunded":
return "已退款"
default:
return status
}
}
const getTypeText = (type: string) => {
switch (type) {
case "service":
return "陪玩服务"
case "product":
return "商城商品"
case "course":
return "课程购买"
case "hotel":
return "酒店预订"
case "cafe":
return "网咖抢座"
case "recharge":
return "充值订单"
case "pawn":
return "账号典当"
default:
return "其他"
}
}
const displayOrders = orders.filter((o) => o.type !== "recharge")
return (
<div className="min-h-screen bg-background pb-24">
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-white/10 rounded-full transition-colors">
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur-sm border-b border-border px-4 py-3 flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-foreground/10 rounded-full transition-colors">
<ArrowLeft size={20} />
</button>
<h1 className="font-bold text-lg"></h1>
@@ -62,7 +91,7 @@ export default function OrdersPage() {
<Tabs defaultValue="all" className="w-full mt-2">
<div className="px-4">
<TabsList className="w-full bg-white/5 p-1 rounded-xl">
<TabsList className="w-full bg-foreground/5 p-1 rounded-xl">
<TabsTrigger value="all" className="flex-1 text-xs">
</TabsTrigger>
@@ -72,21 +101,29 @@ export default function OrdersPage() {
<TabsTrigger value="product" className="flex-1 text-xs">
</TabsTrigger>
<TabsTrigger value="hotel" className="flex-1 text-xs">
</TabsTrigger>
</TabsList>
</div>
{["all", "service", "product"].map((tab) => (
{["all", "service", "product", "hotel"].map((tab) => (
<TabsContent key={tab} value={tab} className="px-4 mt-4 space-y-3">
{orders.filter((o) => tab === "all" || o.type === tab).length > 0 ? (
orders
.filter((o) => tab === "all" || o.type === tab)
{displayOrders.filter(
(o) => tab === "all" || o.type === tab || (tab === "hotel" && (o.type === "hotel" || o.type === "cafe")),
).length > 0 ? (
displayOrders
.filter(
(o) =>
tab === "all" || o.type === tab || (tab === "hotel" && (o.type === "hotel" || o.type === "cafe")),
)
.map((order, i) => (
<div key={i} className="glass-card p-4 rounded-xl">
<div key={i} className="bg-card rounded-xl p-4 border border-border">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-2">
<span className="text-xs text-white/50">{order.date}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-white/5 text-white/70 border border-white/10">
{order.type === "service" ? "陪玩服务" : "商城商品"}
<span className="text-xs text-foreground/50">{order.date}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-foreground/5 text-foreground/70 border border-border">
{getTypeText(order.type)}
</span>
</div>
<div className={`flex items-center gap-1 text-xs font-medium ${getStatusColor(order.status)}`}>
@@ -98,42 +135,48 @@ export default function OrdersPage() {
<div className="flex gap-3">
<div className="relative w-16 h-16 rounded-lg overflow-hidden bg-muted flex-shrink-0">
<Image
src={order.image || "/placeholder.svg"}
src={order.image || "/placeholder.svg?height=64&width=64&query=order"}
alt={order.title}
fill
className="object-cover"
/>
</div>
<div className="flex-1">
<h3 className="font-bold text-sm mb-1">{order.title}</h3>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-sm mb-1 truncate">{order.title}</h3>
<p className="text-xs text-foreground/50 mb-1">: {order.orderNo || order.id}</p>
<div className="flex items-center justify-between mt-2">
<span className="text-xs text-white/50">数量: 1</span>
<span className="text-xs text-foreground/50">数量: 1</span>
<div className="flex items-center gap-1">
<span className="text-xs font-bold">{order.price}</span>
<span className="text-[10px] text-white/50">
{order.currency === "diamonds" ? "钻石" : "丸子币"}
</span>
<span className="text-sm font-bold text-primary">{order.price}</span>
<span className="text-[10px] text-foreground/50"></span>
</div>
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-white/5 flex justify-end gap-2">
<Button
variant="outline"
size="sm"
className="h-7 text-xs border-white/10 hover:bg-white/5 bg-transparent"
>
</Button>
<Button size="sm" className="h-7 text-xs bg-white/10 hover:bg-white/20 border border-white/10">
</Button>
<div className="mt-3 pt-3 border-t border-border flex justify-end gap-2">
<Link href="/customer-service">
<Button
variant="outline"
size="sm"
className="h-7 text-xs border-border hover:bg-foreground/5 bg-transparent"
>
</Button>
</Link>
<Link href={`/order/${order.id}`}>
<Button
size="sm"
className="h-7 text-xs bg-foreground/10 hover:bg-foreground/20 border border-border"
>
</Button>
</Link>
</div>
</div>
))
) : (
<div className="flex flex-col items-center justify-center py-20 text-white/50">
<div className="flex flex-col items-center justify-center py-20 text-foreground/50">
<ShoppingBag size={48} className="mb-4 opacity-20" />
<p></p>
</div>

View File

@@ -163,7 +163,13 @@ export default function HomePage() {
<h1 className="font-bold text-lg tracking-tight text-white"></h1>
</div>
<div className="flex items-center gap-3">
<Link
href="/recharge"
className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/5 border border-white/10 hover:bg-white/10 transition-colors"
>
<Coins size={14} className="text-primary" />
<span className="text-xs font-bold">{wallet.balance}</span>
</Link>
<button className="w-9 h-9 rounded-full bg-white/5 flex items-center justify-center hover:bg-white/10 transition-colors border border-white/5">
<Search size={18} className="text-white/70" />
</button>

276
app/party/[id]/client.tsx Normal file
View File

@@ -0,0 +1,276 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import Image from "next/image"
import {
Mic,
MicOff,
MessageSquare,
Gift,
LogOut,
MoreHorizontal,
Send,
UserPlus,
Users,
Star,
Check,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Input } from "@/components/ui/input"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useAppContext } from "@/components/providers/app-provider"
const PARTY_DATA: Record<string, { name: string; host: string; avatar: string; game: string; background: string }> = {
"1": {
name: "GM-远洋的派对群",
host: "GM-远洋",
avatar: "/streamer-1.jpg",
game: "英雄联盟",
background: "/party-lol.jpg",
},
"2": {
name: "甜心女王粉丝派对",
host: "甜心女王",
avatar: "/streamer-5.jpg",
game: "王者荣耀",
background: "/party-hok.jpg",
},
"3": {
name: "带粉上分车队",
host: "路人王",
avatar: "/streamer-3.jpg",
game: "和平精英",
background: "/party-team.jpg",
},
}
const GIFTS = [
{ id: 1, name: "小心心", price: 1, icon: "💕" },
{ id: 2, name: "棒棒糖", price: 10, icon: "🍭" },
{ id: 3, name: "玫瑰花", price: 52, icon: "🌹" },
{ id: 4, name: "告白气球", price: 520, icon: "🎈" },
{ id: 5, name: "跑车", price: 5000, icon: "🏎️" },
{ id: 6, name: "火箭", price: 10000, icon: "🚀" },
]
export function PartyRoomClient({ partyId }: { partyId: string }) {
const router = useRouter()
const { pay } = useAppContext()
const [isMicOn, setIsMicOn] = useState(false)
const [showGifts, setShowGifts] = useState(false)
const [showGiftSuccess, setShowGiftSuccess] = useState(false)
const [lastGift, setLastGift] = useState<string>("")
const [messages, setMessages] = useState([
{ user: "系统", content: "欢迎来到派对房间", type: "system" },
{ user: "小柠檬", content: "大家好呀~", type: "user" },
{ user: "GM-远洋", content: "今天带大家上分!", type: "user" },
])
const [inputValue, setInputValue] = useState("")
const partyInfo = PARTY_DATA[partyId] || PARTY_DATA["1"]
const seats = [
{ id: 0, user: { name: partyInfo.host, avatar: partyInfo.avatar, isMuted: false, isHost: true } },
{ id: 1, user: { name: "小柠檬", avatar: "/streamer-2.jpg", isMuted: false, isHost: false } },
{ id: 2, user: { name: "电竞萌妹", avatar: "/streamer-6.jpg", isMuted: true, isHost: false } },
{ id: 3, user: null },
{ id: 4, user: null },
{ id: 5, user: null },
{ id: 6, user: null },
{ id: 7, user: null },
]
const handleSendMessage = () => {
if (!inputValue.trim()) return
setMessages([...messages, { user: "我", content: inputValue, type: "user" }])
setInputValue("")
}
const handleSendGift = (gift: (typeof GIFTS)[0]) => {
pay(gift.price, `送出${gift.name}`, "product")
setLastGift(`${gift.icon} ${gift.name}`)
setShowGifts(false)
setShowGiftSuccess(true)
setTimeout(() => setShowGiftSuccess(false), 2000)
}
return (
<div className="min-h-screen bg-black relative flex flex-col">
{/* Background */}
<div className="absolute inset-0 z-0">
<Image
src={partyInfo.background || "/placeholder.svg"}
alt="Background"
fill
className="object-cover opacity-40"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/30 to-black/90" />
</div>
{/* Gift Success Toast */}
{showGiftSuccess && (
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-50 animate-in slide-in-from-top-4 duration-300">
<div className="flex items-center gap-2 bg-gradient-to-r from-pink-500/90 to-purple-500/90 backdrop-blur-md px-4 py-2 rounded-full shadow-lg">
<Check size={16} className="text-white" />
<span className="text-white text-sm font-medium"> {lastGift}</span>
</div>
</div>
)}
{/* Header */}
<header className="relative z-10 px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" className="text-white" onClick={() => router.back()}>
<LogOut className="w-5 h-5" />
</Button>
<div className="flex items-center gap-2">
<div className="relative w-10 h-10 rounded-full overflow-hidden border-2 border-primary">
<Image src={partyInfo.avatar || "/placeholder.svg"} alt={partyInfo.host} fill className="object-cover" />
</div>
<div>
<h1 className="text-white font-bold text-sm flex items-center gap-1">
{partyInfo.name}
<Star size={12} className="text-yellow-400 fill-yellow-400" />
</h1>
<p className="text-white/60 text-[10px] flex items-center gap-2">
<span className="flex items-center gap-1">
<Users size={10} />
125
</span>
<span>{partyInfo.game}</span>
</p>
</div>
</div>
</div>
<Button variant="ghost" size="icon" className="text-white">
<MoreHorizontal className="w-5 h-5" />
</Button>
</header>
{/* Seats Grid */}
<div className="relative z-10 flex-1 px-4 py-6">
<div className="grid grid-cols-4 gap-4 gap-y-8">
{seats.map((seat) => (
<div key={seat.id} className="flex flex-col items-center gap-2">
<div className="relative w-14 h-14">
<div className="w-14 h-14 rounded-full bg-white/10 border border-white/20 flex items-center justify-center overflow-hidden relative">
{seat.user ? (
<Image
src={seat.user.avatar || "/placeholder.svg"}
alt={seat.user.name}
fill
className="object-cover"
/>
) : (
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center">
<UserPlus className="w-4 h-4 text-white/30" />
</div>
)}
</div>
{seat.user?.isMuted && (
<div className="absolute bottom-0 right-0 bg-black/50 rounded-full p-0.5 border border-white/20">
<MicOff className="w-3 h-3 text-red-500" />
</div>
)}
{seat.user?.isHost && (
<div className="absolute -top-1 -right-1 bg-yellow-500 rounded-full p-0.5">
<Star className="w-3 h-3 text-white fill-white" />
</div>
)}
{seat.id === 0 && (
<div className="absolute -inset-1 border-2 border-primary/50 rounded-full animate-pulse" />
)}
</div>
<span className="text-xs text-white/80 truncate max-w-[4rem]">
{seat.user ? seat.user.name : `${seat.id + 1}号麦`}
</span>
</div>
))}
</div>
</div>
{/* Chat Area */}
<div className="relative z-10 h-48 px-4 mb-2">
<ScrollArea className="h-full w-3/4">
<div className="space-y-2 flex flex-col justify-end min-h-full pb-2">
{messages.map((msg, i) => (
<div
key={i}
className={`bg-black/40 backdrop-blur-sm px-3 py-1.5 rounded-xl self-start max-w-full ${msg.type === "system" ? "text-yellow-400" : "text-white"}`}
>
<span className="text-white/60 text-xs mr-2">{msg.user}:</span>
<span className="text-sm">{msg.content}</span>
</div>
))}
</div>
</ScrollArea>
</div>
{/* Bottom Controls */}
<div className="relative z-10 bg-black/80 backdrop-blur-md px-4 py-3 border-t border-white/10 pb-8">
<div className="flex items-center gap-3 mb-3">
<Input
className="bg-white/10 border-none text-white h-9"
placeholder="聊点什么..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSendMessage()}
/>
<Button size="icon" className="bg-primary hover:bg-primary/90 h-9 w-9" onClick={handleSendMessage}>
<Send className="w-4 h-4" />
</Button>
</div>
<div className="flex items-center justify-between px-2">
<div className="flex gap-6">
<div className="flex flex-col items-center gap-1 cursor-pointer" onClick={() => setIsMicOn(!isMicOn)}>
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${isMicOn ? "bg-white text-black" : "bg-white/10 text-white"}`}
>
{isMicOn ? <Mic className="w-5 h-5" /> : <MicOff className="w-5 h-5" />}
</div>
<span className="text-[10px] text-white/60">{isMicOn ? "开麦" : "闭麦"}</span>
</div>
<div className="flex flex-col items-center gap-1 cursor-pointer">
<div className="w-10 h-10 rounded-full bg-white/10 text-white flex items-center justify-center">
<MessageSquare className="w-5 h-5" />
</div>
<span className="text-[10px] text-white/60"></span>
</div>
</div>
<Button
className="rounded-full px-6 bg-gradient-to-r from-pink-500 to-purple-500 border-none shadow-lg shadow-pink-500/20"
onClick={() => setShowGifts(true)}
>
<Gift className="w-5 h-5 mr-2" />
</Button>
</div>
</div>
{/* Gift Dialog */}
<Dialog open={showGifts} onOpenChange={setShowGifts}>
<DialogContent className="bg-zinc-900/95 backdrop-blur-xl border-white/10 text-white max-w-sm">
<DialogHeader>
<DialogTitle> {partyInfo.host}</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-3 gap-3 py-4">
{GIFTS.map((gift) => (
<button
key={gift.id}
onClick={() => handleSendGift(gift)}
className="flex flex-col items-center gap-2 p-3 bg-white/5 rounded-xl hover:bg-white/10 transition-colors border border-white/5 hover:border-pink-500/30 active:scale-95"
>
<span className="text-3xl">{gift.icon}</span>
<span className="text-xs font-bold">{gift.name}</span>
<span className="text-[10px] text-primary">{gift.price} </span>
</button>
))}
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,10 @@
export default function PartyLoading() {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="w-12 h-12 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<p className="text-white/60 text-sm">...</p>
</div>
</div>
)
}

View File

@@ -1,173 +1,6 @@
"use client"
import { PartyRoomClient } from "./client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import Image from "next/image"
import { Mic, MicOff, MessageSquare, Gift, LogOut, MoreHorizontal, Send, UserPlus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { toast } from "@/hooks/use-toast"
import { Input } from "@/components/ui/input"
export default function PartyRoomPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [isMicOn, setIsMicOn] = useState(false)
const [messages, setMessages] = useState([
{ user: "系统", content: "欢迎来到 玩值电竞 派对房间", type: "system" },
{ user: "小柠檬", content: "大家好呀~", type: "user" },
{ user: "GM-远洋", content: "来个唱歌好听的", type: "user" },
])
const [inputValue, setInputValue] = useState("")
const seats = Array(8)
.fill(null)
.map((_, i) => ({
id: i,
user:
i < 3
? {
name: i === 0 ? "房主" : `用户${i}`,
avatar: i === 0 ? "gamer-girl-headphones.jpg" : "abstract-geometric-shapes.png",
isMuted: i === 2,
}
: null,
}))
const handleSendMessage = () => {
if (!inputValue.trim()) return
setMessages([...messages, { user: "我", content: inputValue, type: "user" }])
setInputValue("")
}
return (
<div className="min-h-screen bg-black relative flex flex-col">
{/* Background */}
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1516280440614-6697288d5d38?w=800&q=80"
alt="Background"
fill
className="object-cover opacity-30 blur-xl"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/20 to-black/90" />
</div>
{/* Header */}
<header className="relative z-10 px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" className="text-white" onClick={() => router.back()}>
<LogOut className="w-5 h-5" />
</Button>
<div>
<h1 className="text-white font-bold text-sm">耀</h1>
<p className="text-white/60 text-[10px]">ID: {params.id} | 在线: 125</p>
</div>
</div>
<Button variant="ghost" size="icon" className="text-white">
<MoreHorizontal className="w-5 h-5" />
</Button>
</header>
{/* Seats Grid */}
<div className="relative z-10 flex-1 px-4 py-6">
<div className="grid grid-cols-4 gap-4 gap-y-8">
{seats.map((seat) => (
<div key={seat.id} className="flex flex-col items-center gap-2">
<div className="relative w-14 h-14">
<div className="w-14 h-14 rounded-full bg-white/10 border border-white/20 flex items-center justify-center overflow-hidden relative">
{seat.user ? (
<Image
src={
seat.user.avatar === "gamer-girl-headphones.jpg"
? "/gamer-girl-headphones.jpg"
: `/abstract-geometric-shapes.png?height=100&width=100&query=avatar${seat.id}`
}
alt={seat.user.name}
fill
className="object-cover"
/>
) : (
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center">
<UserPlus className="w-4 h-4 text-white/30" />
</div>
)}
</div>
{seat.user?.isMuted && (
<div className="absolute bottom-0 right-0 bg-black/50 rounded-full p-0.5 border border-white/20">
<MicOff className="w-3 h-3 text-red-500" />
</div>
)}
{/* Wave Animation for talking (mock) */}
{seat.id === 0 && (
<div className="absolute -inset-1 border-2 border-primary/50 rounded-full animate-pulse" />
)}
</div>
<span className="text-xs text-white/80 truncate max-w-[4rem]">
{seat.user ? seat.user.name : `${seat.id + 1}号麦`}
</span>
</div>
))}
</div>
</div>
{/* Chat Area */}
<div className="relative z-10 h-48 px-4 mb-2">
<ScrollArea className="h-full w-3/4 mask-image-linear-gradient-to-t">
<div className="space-y-2 flex flex-col justify-end min-h-full pb-2">
{messages.map((msg, i) => (
<div
key={i}
className={`bg-black/40 backdrop-blur-sm px-3 py-1.5 rounded-xl self-start max-w-full ${msg.type === "system" ? "text-yellow-400" : "text-white"}`}
>
<span className="text-white/60 text-xs mr-2">{msg.user}:</span>
<span className="text-sm">{msg.content}</span>
</div>
))}
</div>
</ScrollArea>
</div>
{/* Bottom Controls */}
<div className="relative z-10 bg-black/80 backdrop-blur-md px-4 py-3 border-t border-white/10 pb-8">
<div className="flex items-center gap-3 mb-3">
<Input
className="bg-white/10 border-none text-white h-9"
placeholder="聊点什么..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSendMessage()}
/>
<Button size="icon" className="bg-primary hover:bg-primary/90 h-9 w-9" onClick={handleSendMessage}>
<Send className="w-4 h-4" />
</Button>
</div>
<div className="flex items-center justify-between px-2">
<div className="flex gap-6">
<div className="flex flex-col items-center gap-1 cursor-pointer" onClick={() => setIsMicOn(!isMicOn)}>
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${isMicOn ? "bg-white text-black" : "bg-white/10 text-white"}`}
>
{isMicOn ? <Mic className="w-5 h-5" /> : <MicOff className="w-5 h-5" />}
</div>
<span className="text-[10px] text-white/60">{isMicOn ? "开麦" : "闭麦"}</span>
</div>
<div className="flex flex-col items-center gap-1 cursor-pointer">
<div className="w-10 h-10 rounded-full bg-white/10 text-white flex items-center justify-center">
<MessageSquare className="w-5 h-5" />
</div>
<span className="text-[10px] text-white/60"></span>
</div>
</div>
<Button
className="rounded-full px-6 bg-gradient-to-r from-pink-500 to-purple-500 border-none shadow-lg shadow-pink-500/20"
onClick={() => toast({ title: "礼物发送", description: "送出一个 🚀 火箭" })}
>
<Gift className="w-5 h-5 mr-2" />
</Button>
</div>
</div>
</div>
)
export default async function PartyRoomPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
return <PartyRoomClient partyId={id} />
}

View File

@@ -8,7 +8,6 @@ import Image from "next/image"
import { useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { useRouter } from "next/navigation"
import { useAppContext } from "@/components/providers/app-provider"
type MatchCategory = "star" | "guild" | "cp" | "coach"
@@ -83,16 +82,6 @@ const matchResultsData = {
intro: "喜欢吃鸡,求带飞~",
online: true,
},
{
id: 3,
name: "游戏小仙女",
title: "元气少女",
avatar: "/viewer-1.jpg",
age: 21,
game: "原神",
intro: "一起探索提瓦特吧",
online: false,
},
],
coach: [
{
@@ -117,23 +106,11 @@ const matchResultsData = {
rating: 4.8,
orders: 1823,
},
{
id: 3,
name: "吃鸡教练",
title: "2000分大神",
avatar: "/streamer-3.jpg",
rank: "无敌战神",
price: 25,
game: "和平精英",
rating: 4.7,
orders: 3102,
},
],
}
export default function PlanetPage() {
const router = useRouter()
const { pay } = useAppContext()
const [isMatching, setIsMatching] = useState(false)
const [selectedCategory, setSelectedCategory] = useState<MatchCategory>("star")
@@ -142,14 +119,13 @@ export default function PlanetPage() {
setTimeout(() => {
setIsMatching(false)
const results = matchResultsData[selectedCategory]
const randomResult = results[Math.floor(Math.random() * results.length)]
// 直接跳转到对应页面
switch (selectedCategory) {
case "star":
router.push(`/star/${randomResult.id}`)
// 直接跳转到明星的派对群聊天室
router.push(`/party/${randomResult.id}`)
break
case "guild":
router.push(`/guild/${randomResult.id}`)
@@ -164,10 +140,6 @@ export default function PlanetPage() {
}, 1500)
}
const handleCategoryChange = (category: MatchCategory) => {
setSelectedCategory(category)
}
const getCategoryLabel = () => {
switch (selectedCategory) {
case "star":
@@ -185,38 +157,36 @@ export default function PlanetPage() {
<div className="pb-24 pt-6 px-4">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-white"></h1>
<div className="flex gap-2">
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full bg-white/5">
<Filter className="w-5 h-5" />
</Button>
</DialogTrigger>
<DialogContent className="bg-zinc-900 border-white/10 text-white">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<h4 className="font-medium leading-none"></h4>
<div className="flex flex-wrap gap-2">
{["全部", "游戏", "聊天", "K歌", "交友"].map((tag) => (
<Button
key={tag}
variant="outline"
size="sm"
className="bg-white/5 border-white/10 hover:bg-primary hover:border-primary hover:text-white"
>
{tag}
</Button>
))}
</div>
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full bg-white/5">
<Filter className="w-5 h-5" />
</Button>
</DialogTrigger>
<DialogContent className="bg-zinc-900 border-white/10 text-white">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<h4 className="font-medium leading-none"></h4>
<div className="flex flex-wrap gap-2">
{["全部", "游戏", "聊天", "K歌", "交友"].map((tag) => (
<Button
key={tag}
variant="outline"
size="sm"
className="bg-white/5 border-white/10 hover:bg-primary hover:border-primary hover:text-white"
>
{tag}
</Button>
))}
</div>
</div>
<Button className="w-full bg-primary hover:bg-primary/90"></Button>
</DialogContent>
</Dialog>
</div>
</div>
<Button className="w-full bg-primary hover:bg-primary/90"></Button>
</DialogContent>
</Dialog>
</div>
<Tabs defaultValue="voice" className="w-full">
@@ -293,10 +263,8 @@ export default function PlanetPage() {
<div className="grid grid-cols-4 gap-4">
<Button
variant="outline"
onClick={() => handleCategoryChange("star")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-primary/50 transition-all ${
selectedCategory === "star" ? "border-primary bg-primary/20 ring-2 ring-primary/50" : ""
}`}
onClick={() => setSelectedCategory("star")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-primary/50 transition-all ${selectedCategory === "star" ? "border-primary bg-primary/20 ring-2 ring-primary/50" : ""}`}
>
<Star
className={`w-7 h-7 mb-2 ${selectedCategory === "star" ? "text-primary" : "text-yellow-500"}`}
@@ -305,10 +273,8 @@ export default function PlanetPage() {
</Button>
<Button
variant="outline"
onClick={() => handleCategoryChange("guild")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-blue-500/50 transition-all ${
selectedCategory === "guild" ? "border-blue-500 bg-blue-500/20 ring-2 ring-blue-500/50" : ""
}`}
onClick={() => setSelectedCategory("guild")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-blue-500/50 transition-all ${selectedCategory === "guild" ? "border-blue-500 bg-blue-500/20 ring-2 ring-blue-500/50" : ""}`}
>
<Users
className={`w-7 h-7 mb-2 ${selectedCategory === "guild" ? "text-blue-400" : "text-blue-500"}`}
@@ -317,20 +283,16 @@ export default function PlanetPage() {
</Button>
<Button
variant="outline"
onClick={() => handleCategoryChange("cp")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-pink-500/50 transition-all ${
selectedCategory === "cp" ? "border-pink-500 bg-pink-500/20 ring-2 ring-pink-500/50" : ""
}`}
onClick={() => setSelectedCategory("cp")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-pink-500/50 transition-all ${selectedCategory === "cp" ? "border-pink-500 bg-pink-500/20 ring-2 ring-pink-500/50" : ""}`}
>
<Heart className={`w-7 h-7 mb-2 ${selectedCategory === "cp" ? "text-pink-400" : "text-pink-500"}`} />
<span className="text-xs font-medium">CP</span>
</Button>
<Button
variant="outline"
onClick={() => handleCategoryChange("coach")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-green-500/50 transition-all ${
selectedCategory === "coach" ? "border-green-500 bg-green-500/20 ring-2 ring-green-500/50" : ""
}`}
onClick={() => setSelectedCategory("coach")}
className={`flex flex-col h-auto py-5 border-white/10 bg-white/5 hover:bg-white/10 hover:border-green-500/50 transition-all ${selectedCategory === "coach" ? "border-green-500 bg-green-500/20 ring-2 ring-green-500/50" : ""}`}
>
<Gamepad2
className={`w-7 h-7 mb-2 ${selectedCategory === "coach" ? "text-green-400" : "text-green-500"}`}
@@ -345,26 +307,37 @@ export default function PlanetPage() {
<TabsContent value="party" className="space-y-4">
{[
{
title: "GM-远洋的粉丝群",
id: 1,
title: "GM-远洋的派对群",
tag: "直播中",
count: 1205,
image: "/esports-tournament-stadium.jpg",
image: "/party-lol.jpg",
isLive: true,
streamer: "GM-远洋",
},
{
id: 2,
title: "甜心女王粉丝派对",
tag: "热门",
count: 2341,
image: "/party-hok.jpg",
isLive: true,
streamer: "甜心女王",
},
{
id: 3,
title: "带粉上分车队",
tag: "游戏",
count: 89,
image: "/gaming-room-rgb.jpg",
image: "/party-team.jpg",
isLive: false,
streamer: "路人王",
},
].map((room, i) => (
].map((room) => (
<Card
key={i}
key={room.id}
className="bg-white/5 border-white/10 overflow-hidden hover:bg-white/10 transition-colors cursor-pointer relative"
onClick={() => router.push(`/chat/${1000 + i}`)}
onClick={() => router.push(`/party/${room.id}`)}
>
<div className="flex p-3 gap-4">
<div className="relative w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 border border-white/10">
@@ -378,8 +351,8 @@ export default function PlanetPage() {
<div className="flex-1 flex flex-col justify-between py-1">
<div>
<h3 className="font-bold text-white line-clamp-1">{room.title}</h3>
<p className="text-xs text-white/60 mt-1 line-clamp-2">
{room.isLive ? `主播 ${room.streamer} 正在直播` : "进群一起开黑"}
<p className="text-xs text-white/60 mt-1">
{room.streamer} {room.isLive ? "正在直播" : "离线"}
</p>
</div>
<div className="flex items-center justify-between mt-2">

155
app/profile/edit/page.tsx Normal file
View File

@@ -0,0 +1,155 @@
"use client"
import { useState } from "react"
import { ArrowLeft, Camera, ChevronRight, Check } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { useRouter } from "next/navigation"
import { useAppContext } from "@/components/providers/app-provider"
import Image from "next/image"
export default function ProfileEditPage() {
const router = useRouter()
const { user, updateUser } = useAppContext()
const [name, setName] = useState(user.name || "")
const [bio, setBio] = useState("")
const [gender, setGender] = useState<"male" | "female" | "unknown">("male")
const [showAvatarPicker, setShowAvatarPicker] = useState(false)
const [selectedAvatar, setSelectedAvatar] = useState(user.avatar || "/gamer-profile-avatar.jpg")
const avatarOptions = [
"/gamer-profile-avatar.jpg",
"/gamer-boy-esports-jersey.jpg",
"/streamer-1.jpg",
"/streamer-2.jpg",
"/streamer-3.jpg",
"/cp-avatar-1.jpg",
"/cp-avatar-2.jpg",
"/coach-avatar-1.jpg",
]
const handleSave = () => {
updateUser({
name,
avatar: selectedAvatar,
})
router.back()
}
return (
<div className="min-h-screen bg-background">
{/* Header */}
<div className="sticky top-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border">
<div className="flex items-center justify-between h-14 px-4">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft size={20} />
</Button>
<h1 className="font-bold"></h1>
<Button variant="ghost" size="sm" onClick={handleSave} className="text-primary">
</Button>
</div>
</div>
<div className="p-4 space-y-6">
{/* Avatar Section */}
<div className="flex flex-col items-center py-6">
<div className="relative" onClick={() => setShowAvatarPicker(true)}>
<div className="w-24 h-24 rounded-full overflow-hidden border-2 border-primary">
<Image
src={selectedAvatar || "/placeholder.svg"}
alt="Avatar"
width={96}
height={96}
className="object-cover"
/>
</div>
<div className="absolute bottom-0 right-0 w-8 h-8 rounded-full bg-primary flex items-center justify-center">
<Camera size={16} className="text-white" />
</div>
</div>
<p className="text-sm text-foreground/50 mt-2"></p>
</div>
{/* Form Fields */}
<div className="space-y-4">
<div className="bg-card rounded-xl border border-border overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-border">
<span className="text-sm text-foreground/70"></span>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
className="bg-transparent border-none text-right w-48 h-auto p-0"
placeholder="请输入昵称"
maxLength={12}
/>
</div>
<div className="flex items-center justify-between p-4 border-b border-border">
<span className="text-sm text-foreground/70">ID</span>
<span className="text-sm text-foreground/50">{user.odlId || "888888"}</span>
</div>
<div className="flex items-center justify-between p-4 border-b border-border">
<span className="text-sm text-foreground/70"></span>
<div className="flex gap-2">
{["male", "female", "unknown"].map((g) => (
<button
key={g}
onClick={() => setGender(g as typeof gender)}
className={`px-3 py-1 rounded-full text-xs transition-colors ${
gender === g ? "bg-primary text-white" : "bg-foreground/10 text-foreground/60"
}`}
>
{g === "male" ? "男" : g === "female" ? "女" : "保密"}
</button>
))}
</div>
</div>
<div className="flex items-center justify-between p-4">
<span className="text-sm text-foreground/70"></span>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground/50 truncate max-w-32">{bio || "这个人很懒,什么都没写"}</span>
<ChevronRight size={16} className="text-foreground/30" />
</div>
</div>
</div>
</div>
</div>
{/* Avatar Picker Modal */}
{showAvatarPicker && (
<div className="fixed inset-0 z-50 bg-black/80 flex items-end">
<div className="bg-card w-full rounded-t-2xl p-4 pb-8">
<div className="flex items-center justify-between mb-4">
<h3 className="font-bold"></h3>
<Button variant="ghost" size="sm" onClick={() => setShowAvatarPicker(false)}>
</Button>
</div>
<div className="grid grid-cols-4 gap-3">
{avatarOptions.map((avatar, i) => (
<button
key={i}
onClick={() => {
setSelectedAvatar(avatar)
setShowAvatarPicker(false)
}}
className="relative aspect-square rounded-full overflow-hidden border-2 transition-colors"
style={{
borderColor: selectedAvatar === avatar ? "rgb(var(--primary))" : "transparent",
}}
>
<Image src={avatar || "/placeholder.svg"} alt={`Avatar ${i}`} fill className="object-cover" />
{selectedAvatar === avatar && (
<div className="absolute inset-0 bg-primary/30 flex items-center justify-center">
<Check size={24} className="text-white" />
</div>
)}
</button>
))}
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -5,7 +5,6 @@ import {
ChevronRight,
FileText,
MessageSquare,
Presentation,
BookOpen,
Bell,
LogOut,
@@ -15,54 +14,62 @@ import {
User,
Medal,
Coins,
Edit3,
Camera,
HelpCircle,
} from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { useAppContext } from "@/components/providers/app-provider"
import { toast } from "@/hooks/use-toast"
import { Card, CardContent } from "@/components/ui/card"
import { useRouter } from "next/navigation"
import { useState } from "react"
export default function ProfilePage() {
const { user, wallet } = useAppContext()
const { user, wallet, logout } = useAppContext()
const router = useRouter()
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false)
const handleComingSoon = (feature: string) => {
toast({
title: "功能开发中",
description: `${feature}即将上线,敬请期待`,
})
const handleLogout = () => {
setShowLogoutConfirm(true)
}
const confirmLogout = () => {
logout()
router.push("/login")
}
return (
<div className="min-h-screen pb-24 bg-black">
<div className="min-h-screen pb-24 bg-background">
{/* Header Area */}
<div className="relative h-72 bg-gradient-to-b from-zinc-800 to-black">
<div className="relative h-72 bg-gradient-to-b from-zinc-800 to-background">
<div className="absolute top-0 inset-x-0 h-full bg-[url('/abstract-esports-pattern.jpg')] opacity-20 bg-cover bg-center mix-blend-overlay" />
<div className="relative z-10 pt-safe px-4">
<div className="flex justify-between items-center h-12 mb-4">
<Button
variant="ghost"
size="icon"
className="text-white/80 hover:text-white hover:bg-white/10"
onClick={() => handleComingSoon("设置")}
>
<Settings size={20} />
</Button>
<Link href="/settings">
<Button
variant="ghost"
size="icon"
className="text-foreground/80 hover:text-foreground hover:bg-foreground/10"
>
<Settings size={20} />
</Button>
</Link>
<div className="flex gap-2">
<Link
href="/recharge"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-foreground/10 hover:bg-foreground/20 transition-colors"
>
<Coins size={14} className="text-primary" />
<span className="text-sm font-bold">{wallet.balance}</span>
<span className="text-xs text-white/50"></span>
<span className="text-xs text-foreground/50"></span>
</Link>
<Link href="/messages">
<Button
variant="ghost"
size="icon"
className="text-white/80 hover:text-white hover:bg-white/10 relative"
className="text-foreground/80 hover:text-foreground hover:bg-foreground/10 relative"
>
<Bell size={20} />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
@@ -72,35 +79,50 @@ export default function ProfilePage() {
</div>
<div className="flex items-center gap-4 mb-6">
<div className="relative w-24 h-24 rounded-full border-2 border-primary p-1 shadow-lg shadow-primary/20">
<Image src="/gamer-profile-avatar.jpg" alt="Profile" fill className="rounded-full object-cover" />
<div className="absolute bottom-0 right-0 bg-gradient-to-r from-primary to-cyan-500 text-[10px] px-2 py-0.5 rounded-full text-white font-bold border-2 border-black">
LV.8
<Link href="/profile/edit" className="relative">
<div className="relative w-24 h-24 rounded-full border-2 border-primary p-1 shadow-lg shadow-primary/20">
<Image
src={user.avatar || "/gamer-profile-avatar.jpg"}
alt="Profile"
fill
className="rounded-full object-cover"
/>
<div className="absolute bottom-0 right-0 bg-gradient-to-r from-primary to-cyan-500 text-[10px] px-2 py-0.5 rounded-full text-white font-bold border-2 border-background">
LV.{user.level || 8}
</div>
</div>
</div>
<div className="absolute -bottom-1 -right-1 w-7 h-7 rounded-full bg-primary flex items-center justify-center">
<Camera size={14} className="text-white" />
</div>
</Link>
<div className="flex-1">
<h2 className="text-xl font-bold text-white mb-1">{user.name}</h2>
<p className="text-xs text-white/60 bg-white/10 px-2 py-0.5 rounded-full inline-block mb-2">
ID: {user.id || "888888"}
<div className="flex items-center gap-2 mb-1">
<h2 className="text-xl font-bold text-foreground">{user.name}</h2>
<Link href="/profile/edit">
<Edit3 size={14} className="text-foreground/50" />
</Link>
</div>
<p className="text-xs text-foreground/60 bg-foreground/10 px-2 py-0.5 rounded-full inline-block mb-2">
ID: {user.odlId || "888888"}
</p>
<p className="text-xs text-white/40"> | 耀</p>
<p className="text-xs text-foreground/40"> | 耀</p>
</div>
</div>
<div className="flex justify-between text-white/90 px-4 bg-white/5 rounded-xl py-3">
<div className="flex justify-between text-foreground/90 px-4 bg-foreground/5 rounded-xl py-3">
<div className="text-center flex-1">
<div className="text-lg font-bold text-primary">{user.activity || 0}</div>
<div className="text-xs text-white/50"></div>
<div className="text-xs text-foreground/50"></div>
</div>
<div className="w-px bg-white/10"></div>
<div className="w-px bg-foreground/10"></div>
<div className="text-center flex-1">
<div className="text-lg font-bold">{user.following || 0}</div>
<div className="text-xs text-white/50"></div>
<div className="text-xs text-foreground/50"></div>
</div>
<div className="w-px bg-white/10"></div>
<div className="w-px bg-foreground/10"></div>
<div className="text-center flex-1">
<div className="text-lg font-bold">{user.followers || 0}</div>
<div className="text-xs text-white/50"></div>
<div className="text-xs text-foreground/50"></div>
</div>
</div>
</div>
@@ -126,96 +148,97 @@ export default function ProfilePage() {
<p className="text-xs text-yellow-100/80">89</p>
</Link>
<Card className="bg-zinc-900/50 border-white/5">
<CardContent className="p-4 grid grid-cols-4 gap-4">
<Link
href="/recharge"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500">
<Wallet size={20} />
</div>
<span className="text-xs text-white/70"></span>
</Link>
<Link
href="/coupons"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500">
<Ticket size={20} />
</div>
<span className="text-xs text-white/70"></span>
</Link>
<Link
href="/footprint"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center text-green-500">
<History size={20} />
</div>
<span className="text-xs text-white/70"></span>
</Link>
<Link
href="/dress-up"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-pink-500/10 flex items-center justify-center text-pink-500">
<User size={20} />
</div>
<span className="text-xs text-white/70"></span>
</Link>
</CardContent>
</Card>
{/* Quick Actions */}
<div className="bg-card border border-border rounded-xl p-4 grid grid-cols-4 gap-4">
<Link
href="/recharge"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500">
<Wallet size={20} />
</div>
<span className="text-xs text-foreground/70"></span>
</Link>
<Link
href="/coupons"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500">
<Ticket size={20} />
</div>
<span className="text-xs text-foreground/70"></span>
</Link>
<Link
href="/transaction-history"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center text-green-500">
<History size={20} />
</div>
<span className="text-xs text-foreground/70"></span>
</Link>
<Link
href="/dress-up"
className="flex flex-col items-center gap-2 cursor-pointer active:scale-95 transition-transform"
>
<div className="w-10 h-10 rounded-full bg-pink-500/10 flex items-center justify-center text-pink-500">
<User size={20} />
</div>
<span className="text-xs text-foreground/70"></span>
</Link>
</div>
{/* Menu Items */}
<div className="space-y-1 rounded-xl overflow-hidden bg-zinc-900/50 border border-white/5">
<div className="space-y-1 rounded-xl overflow-hidden bg-card border border-border">
{[
{ icon: FileText, label: "我的订单", href: "/orders" },
{ icon: BookOpen, label: "我的课程", href: "/my-courses" },
{ icon: Presentation, label: "商业计划", href: "/pitch-deck", hasBadge: true },
{ icon: MessageSquare, label: "联系客服", action: () => handleComingSoon("客服") },
].map((item, i) =>
item.href ? (
<Link
key={i}
href={item.href}
className="w-full flex items-center justify-between p-4 hover:bg-white/5 active:bg-white/10 transition-colors border-b border-white/5 last:border-0"
>
<span className="text-sm text-white flex items-center gap-3">
<item.icon size={18} className="text-white/60" />
{item.label}
</span>
<div className="flex items-center gap-2">
{item.hasBadge && (
<span className="text-[10px] bg-red-500 text-white px-1.5 py-0.5 rounded-full">New</span>
)}
<ChevronRight size={16} className="text-white/30" />
</div>
</Link>
) : (
<button
key={i}
onClick={item.action}
className="w-full flex items-center justify-between p-4 hover:bg-white/5 active:bg-white/10 transition-colors border-b border-white/5 last:border-0"
>
<span className="text-sm text-white flex items-center gap-3">
<item.icon size={18} className="text-white/60" />
{item.label}
</span>
<ChevronRight size={16} className="text-white/30" />
</button>
),
)}
{ icon: HelpCircle, label: "账号与帮助", href: "/account-help" },
{ icon: MessageSquare, label: "联系客服", href: "/customer-service" },
].map((item, i) => (
<Link
key={i}
href={item.href}
className="w-full flex items-center justify-between p-4 hover:bg-foreground/5 active:bg-foreground/10 transition-colors border-b border-border last:border-0"
>
<span className="text-sm text-foreground flex items-center gap-3">
<item.icon size={18} className="text-foreground/60" />
{item.label}
</span>
<ChevronRight size={16} className="text-foreground/30" />
</Link>
))}
</div>
<Button
variant="outline"
className="w-full border-white/10 text-white/50 hover:text-white hover:bg-white/10 hover:border-white/20 bg-transparent"
onClick={() => handleComingSoon("退出登录")}
className="w-full border-border text-foreground/50 hover:text-foreground hover:bg-foreground/10 hover:border-foreground/20 bg-transparent"
onClick={handleLogout}
>
<LogOut size={16} className="mr-2" /> 退
</Button>
</div>
{/* Logout Confirmation Modal */}
{showLogoutConfirm && (
<div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
<div className="bg-card rounded-2xl p-6 w-full max-w-sm border border-border">
<h3 className="text-lg font-bold text-foreground text-center mb-2">退</h3>
<p className="text-sm text-foreground/60 text-center mb-6">退使</p>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1 border-border bg-transparent"
onClick={() => setShowLogoutConfirm(false)}
>
</Button>
<Button className="flex-1 bg-red-500 hover:bg-red-600" onClick={confirmLogout}>
退
</Button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -1,12 +1,18 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { ArrowLeft, Coins, Gem } from "lucide-react"
import { ArrowLeft, Coins, Gem, Check, CreditCard } from "lucide-react"
import { useAppContext } from "@/components/providers/app-provider"
import { Button } from "@/components/ui/button"
export default function RechargePage() {
const router = useRouter()
const { recharge, wallet } = useAppContext()
const [selectedAmount, setSelectedAmount] = useState<number | null>(null)
const [selectedPayment, setSelectedPayment] = useState<"wechat" | "alipay">("wechat")
const [showSuccess, setShowSuccess] = useState(false)
const [purchasedCoins, setPurchasedCoins] = useState(0)
const amounts = [
{ value: 6, coins: 60, bonus: 0 },
@@ -17,12 +23,23 @@ export default function RechargePage() {
{ value: 648, coins: 6480, bonus: 800 },
]
const handleRecharge = (coins: number, bonus: number) => {
recharge(coins + bonus)
const handleSelectAmount = (index: number) => {
setSelectedAmount(index)
}
const handleRecharge = () => {
if (selectedAmount === null) return
const item = amounts[selectedAmount]
const totalCoins = item.coins + item.bonus
recharge(totalCoins)
setPurchasedCoins(totalCoins)
setShowSuccess(true)
}
const selectedItem = selectedAmount !== null ? amounts[selectedAmount] : null
return (
<div className="min-h-screen bg-background pb-12">
<div className="min-h-screen bg-background pb-32">
<header className="sticky top-0 z-40 glass px-4 py-3 flex items-center gap-3">
<button onClick={() => router.back()} className="text-white/70 hover:text-white">
<ArrowLeft size={20} />
@@ -47,65 +64,129 @@ export default function RechargePage() {
</div>
{/* Banner */}
<div className="w-full h-28 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 relative overflow-hidden flex items-center px-6">
<div className="w-full h-24 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 relative overflow-hidden flex items-center px-6">
<div className="relative z-10">
<h2 className="text-xl font-bold text-white mb-1"></h2>
<h2 className="text-lg font-bold text-white mb-1"></h2>
<p className="text-xs text-white/80"> </p>
</div>
<div className="absolute right-0 bottom-0 w-32 h-32 opacity-50">
<Gem className="w-full h-full text-white/20 rotate-12 translate-x-8 translate-y-8" />
<div className="absolute right-0 bottom-0 w-24 h-24 opacity-50">
<Gem className="w-full h-full text-white/20 rotate-12 translate-x-6 translate-y-6" />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
{amounts.map((item) => (
<button
key={item.value}
className="glass-card p-4 rounded-xl flex flex-col items-center justify-center gap-2 hover:bg-white/10 transition-colors border border-white/5 hover:border-primary/50"
onClick={() => handleRecharge(item.coins, item.bonus)}
>
<div className="flex items-center gap-1">
<Coins size={16} className="text-primary" />
<span className="font-bold text-lg">{item.coins}</span>
</div>
<span className="text-sm font-medium text-white/70">¥{item.value}</span>
{item.bonus > 0 && (
<span className="text-[10px] text-yellow-400 bg-yellow-500/10 px-1.5 rounded-full">
{item.bonus}
</span>
)}
</button>
))}
{/* 充值金额选择 */}
<div className="space-y-3">
<h3 className="font-bold text-sm text-white/70"></h3>
<div className="grid grid-cols-3 gap-3">
{amounts.map((item, index) => (
<button
key={item.value}
className={`glass-card p-3 rounded-xl flex flex-col items-center justify-center gap-1.5 transition-all border ${
selectedAmount === index
? "border-primary bg-primary/20 ring-2 ring-primary/50"
: "border-white/5 hover:bg-white/10 hover:border-primary/50"
}`}
onClick={() => handleSelectAmount(index)}
>
<div className="flex items-center gap-1">
<Coins size={14} className="text-primary" />
<span className="font-bold">{item.coins}</span>
</div>
<span className="text-xs text-white/70">¥{item.value}</span>
{item.bonus > 0 && (
<span className="text-[10px] text-yellow-400 bg-yellow-500/10 px-1.5 rounded-full">
{item.bonus}
</span>
)}
</button>
))}
</div>
</div>
{/* 支付方式 */}
<div className="space-y-3">
<h3 className="font-bold text-sm text-white/70"></h3>
<div className="glass-card rounded-xl p-0 overflow-hidden">
<div className="flex items-center gap-3 p-4 border-b border-white/5">
<button
onClick={() => setSelectedPayment("wechat")}
className="w-full flex items-center gap-3 p-4 border-b border-white/5 hover:bg-white/5 transition-colors"
>
<div className="w-8 h-8 rounded-full bg-green-500/20 flex items-center justify-center">
<span className="text-green-500 text-lg"></span>
<span className="text-green-500 text-lg font-bold"></span>
</div>
<div className="flex-1">
<div className="flex-1 text-left">
<div className="font-bold text-sm"></div>
<div className="text-[10px] text-white/40">使</div>
</div>
<div className="w-4 h-4 rounded-full border border-primary bg-primary" />
</div>
<div className="flex items-center gap-3 p-4">
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center">
<span className="text-blue-500 text-lg"></span>
<div
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedPayment === "wechat" ? "border-primary bg-primary" : "border-white/20"
}`}
>
{selectedPayment === "wechat" && <Check size={12} className="text-white" />}
</div>
<div className="flex-1">
</button>
<button
onClick={() => setSelectedPayment("alipay")}
className="w-full flex items-center gap-3 p-4 hover:bg-white/5 transition-colors"
>
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center">
<span className="text-blue-500 text-lg font-bold"></span>
</div>
<div className="flex-1 text-left">
<div className="font-bold text-sm"></div>
</div>
<div className="w-4 h-4 rounded-full border border-white/20" />
</div>
<div
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selectedPayment === "alipay" ? "border-primary bg-primary" : "border-white/20"
}`}
>
{selectedPayment === "alipay" && <Check size={12} className="text-white" />}
</div>
</button>
</div>
</div>
<p className="text-center text-xs text-white/30 pt-4"></p>
<p className="text-center text-xs text-white/30"></p>
</div>
<div className="fixed bottom-16 left-0 right-0 p-4 bg-gradient-to-t from-background via-background to-transparent">
<div className="flex items-center justify-between mb-3">
<span className="text-sm text-white/60"></span>
<span className="text-xl font-bold text-primary">
{selectedItem ? `¥${selectedItem.value}` : "请选择金额"}
</span>
</div>
<Button
onClick={handleRecharge}
disabled={selectedAmount === null}
className="w-full h-12 bg-gradient-to-r from-primary to-cyan-500 hover:from-primary/90 hover:to-cyan-500/90 text-white font-bold text-base rounded-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
<CreditCard size={18} className="mr-2" />
</Button>
</div>
{/* 充值成功弹窗 */}
{showSuccess && (
<div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
<div className="bg-card rounded-2xl p-6 w-full max-w-sm border border-border animate-in zoom-in-95 duration-200">
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-green-500/20 flex items-center justify-center mb-4">
<Check size={32} className="text-green-500" />
</div>
<h3 className="text-lg font-bold text-foreground mb-2"></h3>
<p className="text-sm text-foreground/60 mb-4">
<span className="text-primary font-bold">{purchasedCoins}</span>
</p>
<p className="text-xs text-foreground/40 mb-6">: {wallet.balance} </p>
<Button onClick={() => setShowSuccess(false)} className="w-full bg-primary hover:bg-primary/90">
</Button>
</div>
</div>
</div>
)}
</div>
)
}

120
app/settings/page.tsx Normal file
View File

@@ -0,0 +1,120 @@
"use client"
import { ArrowLeft, ChevronRight, Moon, Bell, Shield, Globe, Info, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { useRouter } from "next/navigation"
import { useState } from "react"
export default function SettingsPage() {
const router = useRouter()
const [darkMode, setDarkMode] = useState(true)
const [notifications, setNotifications] = useState(true)
const [showClearConfirm, setShowClearConfirm] = useState(false)
const handleClearCache = () => {
localStorage.clear()
setShowClearConfirm(false)
}
return (
<div className="min-h-screen bg-background pb-24">
{/* Header */}
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur-sm border-b border-border px-4 py-3 flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-foreground/10 rounded-full transition-colors">
<ArrowLeft size={20} />
</button>
<h1 className="font-bold text-lg"></h1>
</header>
<div className="p-4 space-y-4">
{/* General Settings */}
<div className="bg-card rounded-xl border border-border overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-3">
<Moon size={18} className="text-foreground/60" />
<span className="text-sm"></span>
</div>
<Switch checked={darkMode} onCheckedChange={setDarkMode} />
</div>
<div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-3">
<Bell size={18} className="text-foreground/60" />
<span className="text-sm"></span>
</div>
<Switch checked={notifications} onCheckedChange={setNotifications} />
</div>
<button
onClick={() => router.push("/account-security")}
className="w-full flex items-center justify-between p-4"
>
<div className="flex items-center gap-3">
<Shield size={18} className="text-foreground/60" />
<span className="text-sm"></span>
</div>
<ChevronRight size={16} className="text-foreground/30" />
</button>
</div>
{/* Other Settings */}
<div className="bg-card rounded-xl border border-border overflow-hidden">
<button className="w-full flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center gap-3">
<Globe size={18} className="text-foreground/60" />
<span className="text-sm"></span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-foreground/50"></span>
<ChevronRight size={16} className="text-foreground/30" />
</div>
</button>
<button
onClick={() => setShowClearConfirm(true)}
className="w-full flex items-center justify-between p-4 border-b border-border"
>
<div className="flex items-center gap-3">
<Trash2 size={18} className="text-foreground/60" />
<span className="text-sm"></span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-foreground/50">12.5MB</span>
<ChevronRight size={16} className="text-foreground/30" />
</div>
</button>
<button className="w-full flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Info size={18} className="text-foreground/60" />
<span className="text-sm"></span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-foreground/50">v1.0.0</span>
<ChevronRight size={16} className="text-foreground/30" />
</div>
</button>
</div>
</div>
{/* Clear Cache Confirm */}
{showClearConfirm && (
<div className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
<div className="bg-card rounded-2xl p-6 w-full max-w-sm border border-border">
<h3 className="text-lg font-bold text-foreground text-center mb-2"></h3>
<p className="text-sm text-foreground/60 text-center mb-6"></p>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1 border-border bg-transparent"
onClick={() => setShowClearConfirm(false)}
>
</Button>
<Button className="flex-1 bg-red-500 hover:bg-red-600" onClick={handleClearCache}>
</Button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,97 @@
"use client"
import { ArrowLeft, ArrowUpRight, ArrowDownLeft, Filter } from "lucide-react"
import { useRouter } from "next/navigation"
import { useAppContext } from "@/components/providers/app-provider"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function TransactionHistoryPage() {
const router = useRouter()
const { orders, wallet } = useAppContext()
const incomeOrders = orders.filter((o) => o.type === "recharge")
const expenseOrders = orders.filter((o) => o.type !== "recharge")
return (
<div className="min-h-screen bg-background pb-24">
{/* Header */}
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur-sm border-b border-border px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-foreground/10 rounded-full transition-colors">
<ArrowLeft size={20} />
</button>
<h1 className="font-bold text-lg"></h1>
</div>
<button className="p-2 hover:bg-foreground/10 rounded-full transition-colors">
<Filter size={18} />
</button>
</header>
{/* Balance Card */}
<div className="mx-4 mt-4 p-4 bg-gradient-to-r from-primary to-cyan-500 rounded-xl text-white">
<p className="text-sm opacity-80 mb-1"></p>
<p className="text-3xl font-bold">
{wallet.balance} <span className="text-lg"></span>
</p>
</div>
{/* Tabs */}
<Tabs defaultValue="all" className="w-full mt-4">
<div className="px-4">
<TabsList className="w-full bg-foreground/5 p-1 rounded-xl">
<TabsTrigger value="all" className="flex-1 text-xs">
</TabsTrigger>
<TabsTrigger value="income" className="flex-1 text-xs">
</TabsTrigger>
<TabsTrigger value="expense" className="flex-1 text-xs">
</TabsTrigger>
</TabsList>
</div>
{[
{ key: "all", data: orders },
{ key: "income", data: incomeOrders },
{ key: "expense", data: expenseOrders },
].map(({ key, data }) => (
<TabsContent key={key} value={key} className="px-4 mt-4 space-y-3">
{data.length > 0 ? (
data.map((order, i) => (
<div key={i} className="bg-card rounded-xl p-4 border border-border">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${
order.type === "recharge" ? "bg-green-500/10 text-green-500" : "bg-red-500/10 text-red-500"
}`}
>
{order.type === "recharge" ? <ArrowDownLeft size={20} /> : <ArrowUpRight size={20} />}
</div>
<div>
<p className="text-sm font-medium">{order.title}</p>
<p className="text-xs text-foreground/50">{order.date}</p>
</div>
</div>
<div className="text-right">
<p className={`font-bold ${order.type === "recharge" ? "text-green-500" : "text-foreground"}`}>
{order.type === "recharge" ? "+" : "-"}
{order.price}
</p>
<p className="text-[10px] text-foreground/40"></p>
</div>
</div>
</div>
))
) : (
<div className="flex flex-col items-center justify-center py-20 text-foreground/50">
<p></p>
</div>
)}
</TabsContent>
))}
</Tabs>
</div>
)
}

View File

@@ -1,97 +1,254 @@
"use client"
import type React from "react"
import { createContext, useContext, useState, useEffect } from "react"
import { createContext, useContext, useState, useEffect, useCallback } from "react"
import { toast } from "@/hooks/use-toast"
import { db } from "@/lib/db/schema"
type User = {
id?: number
name: string
avatar: string
level: number
isVip: boolean
vipType?: "month" | "quarter" | "year"
vipExpiry?: string
id?: string
odlId?: string
phone?: string
activity?: number
following?: number
followers?: number
visitors?: number
isLoggedIn?: boolean
}
type Wallet = {
balance: number // 玩值币 - 唯一货币
balance: number
}
type Order = {
id: string
type: "service" | "product"
type: "service" | "product" | "recharge" | "hotel" | "cafe" | "course" | "pawn"
title: string
price: number
status: "pending" | "completed" | "cancelled"
status: "pending" | "completed" | "cancelled" | "refunded"
date: string
image?: string
orderNo?: string
}
type AppContextType = {
user: User
wallet: Wallet
orders: Order[]
isLoggedIn: boolean
recharge: (amount: number) => void
pay: (amount: number, title: string, type: "service" | "product", image?: string) => boolean
pay: (amount: number, title: string, type: Order["type"], image?: string) => boolean
updateUser: (updates: Partial<User>) => void
login: (phone: string, password?: string) => Promise<boolean>
logout: () => void
syncFromDatabase: () => Promise<void>
saveToDatabase: () => Promise<void>
}
const AppContext = createContext<AppContextType | undefined>(undefined)
export function AppProvider({ children }: { children: React.ReactNode }) {
const [isLoggedIn, setIsLoggedIn] = useState(true)
const [user, setUser] = useState<User>({
name: "卡若",
name: "电竞玩家",
avatar: "/gamer-boy-esports-jersey.jpg",
level: 12,
isVip: false,
id: "888888",
odlId: "888888",
activity: 128,
following: 45,
followers: 892,
visitors: 1205,
isLoggedIn: true,
})
const [wallet, setWallet] = useState<Wallet>({
balance: 6880, // 统一的玩值币余额
balance: 6880,
})
const [orders, setOrders] = useState<Order[]>([
{
id: "1",
orderNo: "WZ20240115001",
type: "service",
title: "LOL陪玩 - 1小时",
price: 50,
title: "王者荣耀陪玩 - 2小时",
price: 80,
status: "completed",
date: "2023-11-15",
date: "2024-01-15",
image: "/王者荣耀.jpg",
},
{
id: "2",
orderNo: "WZ20240114002",
type: "course",
title: "打野进阶课:从入门到王者",
price: 299,
status: "completed",
date: "2024-01-14",
image: "/lol.jpg",
},
{
id: "3",
orderNo: "WZ20240113003",
type: "hotel",
title: "玩值电竞酒店(旗舰店) - 1晚",
price: 299,
status: "completed",
date: "2024-01-13",
image: "/hotel-1.jpg",
},
])
useEffect(() => {
const savedWallet = localStorage.getItem("wanzhi_wallet_v2")
if (savedWallet) setWallet(JSON.parse(savedWallet))
const syncFromDatabase = useCallback(async () => {
try {
const dbUser = await db.users.get(1)
if (dbUser) {
setUser({
id: dbUser.id,
name: dbUser.name,
avatar: dbUser.avatar,
level: dbUser.level,
isVip: dbUser.isVip,
vipType: dbUser.vipType,
vipExpiry: dbUser.vipExpiry,
odlId: dbUser.odlId,
phone: dbUser.phone,
activity: dbUser.activity,
following: dbUser.following,
followers: dbUser.followers,
visitors: dbUser.visitors,
isLoggedIn: true,
})
setWallet({ balance: dbUser.balance })
setIsLoggedIn(true)
}
const savedOrders = localStorage.getItem("wanzhi_orders")
if (savedOrders) setOrders(JSON.parse(savedOrders))
// 加载订单
const dbOrders = await db.transactions.where("userId").equals(1).reverse().sortBy("createdAt")
if (dbOrders.length > 0) {
const mappedOrders: Order[] = dbOrders.slice(0, 20).map((t) => ({
id: String(t.id),
orderNo: t.orderNo,
type: t.category === "充值" ? "recharge" : t.category === "服务消费" ? "service" : "product",
title: t.description,
price: Math.abs(t.amount),
status: t.status === "completed" ? "completed" : "pending",
date: t.createdAt.split("T")[0],
}))
setOrders((prev) => [...mappedOrders, ...prev.filter((o) => !mappedOrders.find((m) => m.id === o.id))])
}
} catch (err) {
console.error("[v0] Sync from database error:", err)
}
}, [])
const saveToDatabase = useCallback(async () => {
try {
const dbUser = await db.users.get(1)
if (dbUser) {
await db.users.update(1, {
name: user.name,
avatar: user.avatar,
level: user.level,
isVip: user.isVip,
vipType: user.vipType,
vipExpiry: user.vipExpiry,
balance: wallet.balance,
activity: user.activity,
following: user.following,
followers: user.followers,
visitors: user.visitors,
updatedAt: new Date().toISOString(),
})
}
} catch (err) {
console.error("[v0] Save to database error:", err)
}
}, [user, wallet])
useEffect(() => {
const timer = setTimeout(() => {
syncFromDatabase()
}, 500)
return () => clearTimeout(timer)
}, [syncFromDatabase])
useEffect(() => {
localStorage.setItem("wanzhi_wallet_v2", JSON.stringify(wallet))
db.users
.get(1)
.then((dbUser) => {
if (dbUser) {
db.users.update(1, { balance: wallet.balance, updatedAt: new Date().toISOString() })
}
})
.catch(() => {})
}, [wallet])
useEffect(() => {
localStorage.setItem("wanzhi_orders", JSON.stringify(orders))
}, [orders])
const recharge = (amount: number) => {
setWallet((prev) => ({ ...prev, balance: prev.balance + amount }))
const login = async (phone: string, password?: string): Promise<boolean> => {
try {
// 模拟登录验证
if (phone.length === 11) {
setIsLoggedIn(true)
setUser((prev) => ({ ...prev, phone, isLoggedIn: true }))
await db.users.update(1, { phone, lastLoginTime: new Date().toISOString() })
return true
}
return false
} catch {
return false
}
}
const pay = (amount: number, title: string, type: "service" | "product", image?: string): boolean => {
const logout = () => {
setIsLoggedIn(false)
setUser((prev) => ({ ...prev, isLoggedIn: false }))
localStorage.removeItem("wanzhi_wallet_v2")
localStorage.removeItem("wanzhi_orders")
}
const recharge = (amount: number) => {
const orderNo = `RCH${Date.now()}`
setWallet((prev) => ({ ...prev, balance: prev.balance + amount }))
const newOrder: Order = {
id: Math.random().toString(36).substr(2, 9),
orderNo,
type: "recharge",
title: `充值 ${amount} 玩值币`,
price: amount,
status: "completed",
date: new Date().toISOString().split("T")[0],
}
setOrders((prev) => [newOrder, ...prev])
db.transactions
.add({
userId: 1,
orderNo,
type: "recharge",
category: "充值",
amount,
balanceBefore: wallet.balance,
balanceAfter: wallet.balance + amount,
description: `充值 ${amount} 玩值币`,
status: "completed",
createdAt: new Date().toISOString(),
})
.catch(() => {})
}
const pay = (amount: number, title: string, type: Order["type"], image?: string): boolean => {
if (wallet.balance < amount) {
toast({
title: "余额不足",
@@ -101,10 +258,12 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
return false
}
const orderNo = `PAY${Date.now()}`
setWallet((prev) => ({ ...prev, balance: prev.balance - amount }))
const newOrder: Order = {
id: Math.random().toString(36).substr(2, 9),
orderNo,
type,
title,
price: amount,
@@ -114,15 +273,64 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
}
setOrders((prev) => [newOrder, ...prev])
db.transactions
.add({
userId: 1,
orderNo,
type: "purchase",
category:
type === "service"
? "服务消费"
: type === "hotel"
? "酒店预订"
: type === "cafe"
? "网咖抢座"
: type === "course"
? "课程购买"
: "商品购买",
amount: -amount,
balanceBefore: wallet.balance,
balanceAfter: wallet.balance - amount,
description: title,
status: "completed",
createdAt: new Date().toISOString(),
})
.catch(() => {})
return true
}
const updateUser = (updates: Partial<User>) => {
setUser((prev) => ({ ...prev, ...updates }))
db.users
.get(1)
.then((dbUser) => {
if (dbUser) {
db.users.update(1, { ...updates, updatedAt: new Date().toISOString() })
}
})
.catch(() => {})
}
return (
<AppContext.Provider value={{ user, wallet, orders, recharge, pay, updateUser }}>{children}</AppContext.Provider>
<AppContext.Provider
value={{
user,
wallet,
orders,
isLoggedIn,
recharge,
pay,
updateUser,
login,
logout,
syncFromDatabase,
saveToDatabase,
}}
>
{children}
</AppContext.Provider>
)
}

View File

@@ -0,0 +1,63 @@
"use client"
import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
import Dexie from "dexie"
interface DatabaseContextType {
isReady: boolean
error: Error | null
}
const DatabaseContext = createContext<DatabaseContextType>({
isReady: false,
error: null,
})
export function DatabaseProvider({ children }: { children: ReactNode }) {
const [isReady, setIsReady] = useState(false)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const initDatabase = async () => {
try {
// 动态导入数据库模块
const { db } = await import("@/lib/db/schema")
const { seedDatabase } = await import("@/lib/db/seed")
await db.open()
await seedDatabase()
setIsReady(true)
} catch (err) {
console.error("[v0] Database init error:", err)
// 如果IndexedDB不可用仍然允许应用运行
if (err instanceof Dexie.MissingAPIError) {
console.warn("[v0] IndexedDB not available, using fallback")
setIsReady(true)
} else {
setError(err as Error)
// 即使出错也允许应用运行
setIsReady(true)
}
}
}
initDatabase()
}, [])
if (!isReady) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<div className="w-8 h-8 border-2 border-cyan-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-muted-foreground text-sm">...</p>
</div>
</div>
)
}
return <DatabaseContext.Provider value={{ isReady, error }}>{children}</DatabaseContext.Provider>
}
export function useDatabaseContext() {
return useContext(DatabaseContext)
}

53
components/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,53 @@
'use client'
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cn } from '@/lib/utils'
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn('aspect-square size-full', className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className,
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

46
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,46 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span'
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,257 @@
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: 'default' | 'destructive'
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

31
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,31 @@
'use client'
import * as React from 'react'
import * as SwitchPrimitive from '@radix-ui/react-switch'
import { cn } from '@/lib/utils'
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'
}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

462
lib/db/hooks.ts Normal file
View File

@@ -0,0 +1,462 @@
// 数据库操作Hooks
"use client"
import { useState, useEffect, useCallback } from "react"
import { db } from "./schema"
import { seedDatabase } from "./seed"
import type {
User,
Streamer,
Star,
Guild,
Companion,
EsportsVenue,
Product,
Transaction,
Moment,
Message,
Notification,
} from "./schema"
// 初始化数据库
export function useInitDatabase() {
const [isReady, setIsReady] = useState(false)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const init = async () => {
try {
await db.open()
await seedDatabase()
setIsReady(true)
} catch (err) {
console.error("[v0] Database init error:", err)
setError(err as Error)
}
}
init()
}, [])
return { isReady, error }
}
// 用户相关
export function useCurrentUser() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadUser = async () => {
try {
const currentUser = await db.users.get(1) // 默认用户ID为1
setUser(currentUser || null)
} catch (err) {
console.error("[v0] Load user error:", err)
} finally {
setLoading(false)
}
}
loadUser()
}, [])
const updateUser = useCallback(
async (updates: Partial<User>) => {
if (!user?.id) return
await db.users.update(user.id, { ...updates, updatedAt: new Date().toISOString() })
setUser((prev) => (prev ? { ...prev, ...updates } : null))
},
[user?.id],
)
const updateBalance = useCallback(
async (amount: number, type: "add" | "subtract") => {
if (!user?.id) return false
const newBalance = type === "add" ? user.balance + amount : user.balance - amount
if (newBalance < 0) return false
await db.users.update(user.id, { balance: newBalance, updatedAt: new Date().toISOString() })
setUser((prev) => (prev ? { ...prev, balance: newBalance } : null))
return true
},
[user],
)
return { user, loading, updateUser, updateBalance }
}
// 主播列表
export function useStreamers(options?: { game?: string; isLive?: boolean; limit?: number }) {
const [streamers, setStreamers] = useState<Streamer[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
let query = db.streamers.where("status").equals("approved")
if (options?.isLive !== undefined) {
query = db.streamers.where("isLive").equals(options.isLive ? 1 : 0)
}
let results = await query.toArray()
if (options?.game) {
results = results.filter((s) => s.game === options.game)
}
if (options?.limit) {
results = results.slice(0, options.limit)
}
setStreamers(results)
} catch (err) {
console.error("[v0] Load streamers error:", err)
} finally {
setLoading(false)
}
}
load()
}, [options?.game, options?.isLive, options?.limit])
return { streamers, loading }
}
// 明星列表
export function useStars(options?: { game?: string; limit?: number }) {
const [stars, setStars] = useState<Star[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
let results = await db.stars.where("status").equals("active").toArray()
if (options?.game) {
results = results.filter((s) => s.game === options.game)
}
if (options?.limit) {
results = results.slice(0, options.limit)
}
setStars(results)
} catch (err) {
console.error("[v0] Load stars error:", err)
} finally {
setLoading(false)
}
}
load()
}, [options?.game, options?.limit])
return { stars, loading }
}
// 公会列表
export function useGuilds(options?: { limit?: number }) {
const [guilds, setGuilds] = useState<Guild[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
let results = await db.guilds.where("status").equals("active").toArray()
if (options?.limit) {
results = results.slice(0, options.limit)
}
setGuilds(results)
} catch (err) {
console.error("[v0] Load guilds error:", err)
} finally {
setLoading(false)
}
}
load()
}, [options?.limit])
return { guilds, loading }
}
// CP/陪练列表
export function useCompanions(options?: { category?: "cp" | "coach"; gender?: string; limit?: number }) {
const [companions, setCompanions] = useState<Companion[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
let results = await db.companions.where("status").equals("active").toArray()
if (options?.category) {
results = results.filter((c) => c.category === options.category)
}
if (options?.gender) {
results = results.filter((c) => c.gender === options.gender)
}
if (options?.limit) {
results = results.slice(0, options.limit)
}
setCompanions(results)
} catch (err) {
console.error("[v0] Load companions error:", err)
} finally {
setLoading(false)
}
}
load()
}, [options?.category, options?.gender, options?.limit])
return { companions, loading }
}
// 电竞酒店/网咖
export function useVenues(options?: { type?: "hotel" | "cafe"; city?: string; limit?: number }) {
const [venues, setVenues] = useState<EsportsVenue[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
let results = await db.esportsVenues.where("status").equals("active").toArray()
if (options?.type) {
results = results.filter((v) => v.type === options.type)
}
if (options?.city) {
results = results.filter((v) => v.city === options.city)
}
if (options?.limit) {
results = results.slice(0, options.limit)
}
setVenues(results)
} catch (err) {
console.error("[v0] Load venues error:", err)
} finally {
setLoading(false)
}
}
load()
}, [options?.type, options?.city, options?.limit])
return { venues, loading }
}
// 商品列表
export function useProducts(options?: { category?: string; game?: string; limit?: number }) {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
let results = await db.products.where("status").equals("active").toArray()
if (options?.category) {
results = results.filter((p) => p.category === options.category)
}
if (options?.game) {
results = results.filter((p) => p.game === options.game)
}
if (options?.limit) {
results = results.slice(0, options.limit)
}
setProducts(results)
} catch (err) {
console.error("[v0] Load products error:", err)
} finally {
setLoading(false)
}
}
load()
}, [options?.category, options?.game, options?.limit])
return { products, loading }
}
// 交易记录
export function useTransactions(userId: number, options?: { type?: string; limit?: number }) {
const [transactions, setTransactions] = useState<Transaction[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
let results = await db.transactions.where("userId").equals(userId).toArray()
if (options?.type) {
results = results.filter((t) => t.type === options.type)
}
results = results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
if (options?.limit) {
results = results.slice(0, options.limit)
}
setTransactions(results)
} catch (err) {
console.error("[v0] Load transactions error:", err)
} finally {
setLoading(false)
}
}
load()
}, [userId, options?.type, options?.limit])
return { transactions, loading }
}
// 动态列表
export function useMoments(options?: { userId?: number; limit?: number }) {
const [moments, setMoments] = useState<Moment[]>([])
const [loading, setLoading] = useState(true)
const loadMoments = useCallback(async () => {
try {
let results = await db.moments.where("status").equals("active").toArray()
if (options?.userId) {
results = results.filter((m) => m.userId === options.userId)
}
results = results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
if (options?.limit) {
results = results.slice(0, options.limit)
}
setMoments(results)
} catch (err) {
console.error("[v0] Load moments error:", err)
} finally {
setLoading(false)
}
}, [options?.userId, options?.limit])
useEffect(() => {
loadMoments()
}, [loadMoments])
const likeMoment = useCallback(
async (momentId: number) => {
const moment = await db.moments.get(momentId)
if (moment) {
await db.moments.update(momentId, {
likes: moment.likes + 1,
isLiked: true,
updatedAt: new Date().toISOString(),
})
await loadMoments()
}
},
[loadMoments],
)
return { moments, loading, likeMoment, refresh: loadMoments }
}
// 消息列表
export function useMessages(conversationId: number) {
const [messages, setMessages] = useState<Message[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
const results = await db.messages.where("conversationId").equals(conversationId).toArray()
setMessages(results.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()))
} catch (err) {
console.error("[v0] Load messages error:", err)
} finally {
setLoading(false)
}
}
load()
}, [conversationId])
const sendMessage = useCallback(
async (content: string, type: "text" | "image" | "voice" = "text") => {
const newMessage: Omit<Message, "id"> = {
conversationId,
senderId: 1, // 当前用户
receiverId: 0, // 需要从conversation获取
type,
content,
isRead: false,
createdAt: new Date().toISOString(),
}
const id = await db.messages.add(newMessage as Message)
setMessages((prev) => [...prev, { ...newMessage, id } as Message])
},
[conversationId],
)
return { messages, loading, sendMessage }
}
// 通知列表
export function useNotifications(userId: number) {
const [notifications, setNotifications] = useState<Notification[]>([])
const [unreadCount, setUnreadCount] = useState(0)
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
const results = await db.notifications.where("userId").equals(userId).toArray()
const sorted = results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
setNotifications(sorted)
setUnreadCount(sorted.filter((n) => !n.isRead).length)
} catch (err) {
console.error("[v0] Load notifications error:", err)
} finally {
setLoading(false)
}
}
load()
}, [userId])
const markAsRead = useCallback(async (notificationId: number) => {
await db.notifications.update(notificationId, { isRead: true })
setNotifications((prev) => prev.map((n) => (n.id === notificationId ? { ...n, isRead: true } : n)))
setUnreadCount((prev) => Math.max(0, prev - 1))
}, [])
const markAllAsRead = useCallback(async () => {
const unread = notifications.filter((n) => !n.isRead)
await Promise.all(unread.map((n) => db.notifications.update(n.id!, { isRead: true })))
setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })))
setUnreadCount(0)
}, [notifications])
return { notifications, unreadCount, loading, markAsRead, markAllAsRead }
}
// 创建订单
export function useCreateOrder() {
const createTransaction = useCallback(
async (
userId: number,
type: Transaction["type"],
category: string,
amount: number,
description: string,
relatedId?: number,
relatedType?: string,
) => {
const user = await db.users.get(userId)
if (!user) throw new Error("User not found")
const balanceBefore = user.balance
const balanceAfter =
type === "recharge" || type === "income" || type === "refund" ? balanceBefore + amount : balanceBefore - amount
if (balanceAfter < 0) {
throw new Error("Insufficient balance")
}
const transaction: Omit<Transaction, "id"> = {
userId,
orderNo: `TXN${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
type,
category,
amount,
balanceBefore,
balanceAfter,
relatedId,
relatedType,
description,
status: "completed",
createdAt: new Date().toISOString(),
}
await db.transactions.add(transaction as Transaction)
await db.users.update(userId, { balance: balanceAfter, updatedAt: new Date().toISOString() })
return transaction
},
[],
)
return { createTransaction }
}

5
lib/db/index.ts Normal file
View File

@@ -0,0 +1,5 @@
// 数据库模块统一导出
export { db, WanzhiDatabase } from "./schema"
export type * from "./schema"
export { seedDatabase, clearDatabase } from "./seed"
export * from "./hooks"

881
lib/db/schema.ts Normal file
View File

@@ -0,0 +1,881 @@
// 玩值电竞 - 完整数据库模式定义
// 使用 Dexie.js (IndexedDB) 作为本地数据库
import Dexie, { type Table } from "dexie"
// ==================== 用户相关 ====================
export interface User {
id?: number
odlId: string // 旧版用户ID兼容
phone: string
password?: string // 加密存储
name: string
avatar: string
level: number
experience: number
isVip: boolean
vipType?: "month" | "quarter" | "year"
vipExpiry?: string
gender: "male" | "female" | "unknown"
birthday?: string
bio?: string
tags: string[]
activity: number
following: number
followers: number
visitors: number
balance: number // 玩值币余额
totalRecharge: number // 总充值金额
totalSpent: number // 总消费金额
inviteCode?: string
invitedBy?: string
registerSource: string
registerTime: string
lastLoginTime: string
status: "active" | "banned" | "deleted"
createdAt: string
updatedAt: string
}
export interface UserAuth {
id?: number
userId: number
authType: "phone" | "wechat" | "qq" | "weibo"
authId: string // 第三方ID或手机号
accessToken?: string
refreshToken?: string
expiresAt?: string
createdAt: string
}
export interface UserFollow {
id?: number
userId: number
followUserId: number
followType: "user" | "streamer" | "star"
createdAt: string
}
// ==================== 主播相关 ====================
export interface Streamer {
id?: number
userId: number // 关联用户
name: string
avatar: string
coverImage: string
title: string
game: string
gameId: number
tags: string[]
level: number
fans: number
hotValue: number // 热度值
isLive: boolean
liveRoomId?: string
totalGifts: number
totalIncome: number
guildId?: number // 所属公会
commissionRate: number // 佣金比例
status: "pending" | "approved" | "rejected" | "suspended"
verifiedAt?: string
createdAt: string
updatedAt: string
}
export interface StreamerCourse {
id?: number
streamerId: number
title: string
description: string
coverImage: string
price: number
originalPrice: number
duration: number // 总时长(分钟)
lessonsCount: number
studentsCount: number
rating: number
game: string
category: string
tags: string[]
lessons: CourseLesson[]
status: "draft" | "published" | "offline"
createdAt: string
updatedAt: string
}
export interface CourseLesson {
id: number
title: string
duration: number // 分钟
isFree: boolean
videoUrl?: string
order: number
}
export interface CoursePurchase {
id?: number
userId: number
courseId: number
streamerId: number
price: number
progress: number // 学习进度百分比
lastWatchedLesson?: number
purchasedAt: string
completedAt?: string
}
// ==================== 公会/俱乐部 ====================
export interface Guild {
id?: number
name: string
logo: string
coverImage: string
description: string
ownerId: number
memberCount: number
maxMembers: number
level: number
totalIncome: number
commissionRate: number // 平台抽成比例
tags: string[]
games: string[]
requirements: string
benefits: string[]
status: "active" | "suspended" | "dissolved"
createdAt: string
updatedAt: string
}
export interface GuildMember {
id?: number
guildId: number
userId: number
role: "owner" | "admin" | "member"
contribution: number // 贡献值
monthlyIncome: number
joinedAt: string
status: "active" | "left" | "kicked"
}
export interface GuildApplication {
id?: number
guildId: number
userId: number
message: string
status: "pending" | "approved" | "rejected"
reviewedBy?: number
reviewedAt?: string
createdAt: string
}
// ==================== 明星/大神 ====================
export interface Star {
id?: number
userId: number
name: string
avatar: string
coverImage: string
title: string
game: string
gameId: number
tags: string[]
fans: number
hotValue: number // 热度值(万)
achievements: string[]
teamName?: string
socialLinks: {
weibo?: string
douyin?: string
bilibili?: string
}
partyRoomId?: string // 派对群ID
hourlyRate: number // 每小时价格
isOnline: boolean
status: "active" | "suspended"
verifiedAt: string
createdAt: string
updatedAt: string
}
export interface StarPartyRoom {
id?: number
starId: number
name: string
memberCount: number
maxMembers: number
entryFee: number
description: string
rules: string[]
isActive: boolean
createdAt: string
}
export interface StarPartyMember {
id?: number
partyRoomId: number
userId: number
role: "host" | "vip" | "member"
joinedAt: string
expiredAt?: string
}
// ==================== CP/陪练 ====================
export interface Companion {
id?: number
userId: number
name: string
avatar: string
coverImage: string
gender: "male" | "female"
age: number
tags: string[]
games: string[]
voiceSample?: string
bio: string
price: number // 每小时价格
originalPrice: number
rating: number
ordersCount: number
responseRate: number
onlineStatus: "online" | "busy" | "offline"
category: "cp" | "coach" // CP或陪练
coachType?: "pro" | "master" // 职业选手/王者大神
rank?: string // 游戏段位
guildId?: number
commissionRate: number
status: "active" | "suspended" | "reviewing"
createdAt: string
updatedAt: string
}
export interface CompanionOrder {
id?: number
orderNo: string
userId: number
companionId: number
type: "cp" | "coach"
game: string
hours: number
price: number
totalAmount: number
message?: string
status: "pending" | "accepted" | "in_progress" | "completed" | "cancelled" | "refunded"
startTime?: string
endTime?: string
rating?: number
review?: string
createdAt: string
updatedAt: string
}
// ==================== 星球匹配 ====================
export interface PlanetMatch {
id?: number
userId: number
matchType: "star" | "guild" | "cp" | "coach"
preferences: {
games?: string[]
gender?: string
priceRange?: [number, number]
tags?: string[]
}
matchedId?: number // 匹配结果ID
matchScore?: number // 匹配度
status: "matching" | "matched" | "cancelled"
createdAt: string
matchedAt?: string
}
// ==================== 直播相关 ====================
export interface LiveRoom {
id?: number
streamerId: number
title: string
coverImage: string
game: string
viewers: number
peakViewers: number
likes: number
gifts: LiveGift[]
status: "live" | "offline" | "replay"
startTime?: string
endTime?: string
duration?: number
createdAt: string
}
export interface LiveGift {
id?: number
liveRoomId: number
userId: number
giftId: number
giftName: string
giftIcon: string
price: number
quantity: number
totalAmount: number
createdAt: string
}
export interface LiveComment {
id?: number
liveRoomId: number
userId: number
userName: string
userAvatar: string
content: string
type: "normal" | "gift" | "enter" | "follow"
createdAt: string
}
// ==================== 商城相关 ====================
export interface Product {
id?: number
name: string
description: string
image: string
images: string[]
category: "skin" | "equipment" | "points" | "peripheral" | "gift"
game?: string
price: number
originalPrice: number
stock: number
sales: number
rating: number
tags: string[]
specifications?: ProductSpec[]
status: "active" | "soldout" | "offline"
createdAt: string
updatedAt: string
}
export interface ProductSpec {
name: string
options: string[]
}
export interface CartItem {
id?: number
userId: number
productId: number
quantity: number
selectedSpecs?: Record<string, string>
createdAt: string
}
export interface ProductOrder {
id?: number
orderNo: string
userId: number
items: OrderItem[]
totalAmount: number
discountAmount: number
finalAmount: number
status: "pending" | "paid" | "shipped" | "completed" | "cancelled" | "refunded"
shippingAddress?: ShippingAddress
trackingNo?: string
paidAt?: string
shippedAt?: string
completedAt?: string
createdAt: string
updatedAt: string
}
export interface OrderItem {
productId: number
productName: string
productImage: string
price: number
quantity: number
specs?: Record<string, string>
}
export interface ShippingAddress {
name: string
phone: string
province: string
city: string
district: string
address: string
isDefault: boolean
}
// ==================== 游戏点卡 ====================
export interface GamePointCard {
id?: number
game: string
gameId: number
gameLogo: string
name: string
description: string
faceValue: number // 面值
price: number
discount: number
stock: number
sales: number
deliveryType: "instant" | "manual"
instructions: string
status: "active" | "soldout" | "offline"
createdAt: string
}
export interface PointCardOrder {
id?: number
orderNo: string
userId: number
cardId: number
game: string
faceValue: number
price: number
quantity: number
totalAmount: number
cardCodes?: string[] // 卡密
status: "pending" | "paid" | "delivered" | "used" | "refunded"
createdAt: string
deliveredAt?: string
}
// ==================== 账号交易/典当 ====================
export interface GameAccount {
id?: number
sellerId: number
game: string
gameId: number
server: string
title: string
description: string
images: string[]
level: number
rank?: string
skins: string[]
heroes: string[]
assets: Record<string, number> // 游戏资产
price: number
originalPrice?: number
evaluatedPrice?: number // AI评估价
status: "reviewing" | "listed" | "sold" | "offline" | "pawned"
viewCount: number
favoriteCount: number
createdAt: string
updatedAt: string
}
export interface AccountPawn {
id?: number
userId: number
accountId: number
game: string
evaluatedValue: number // 评估价值
pawnAmount: number // 典当金额
serviceFee: number // 服务费
redeemAmount: number // 赎回金额
period: 7 | 15 | 30 // 典当期限(天)
phone: string
screenshots: string[]
antiAddiction: "none" | "limited" | "full"
canRename: "yes" | "no" | "once"
selectedSkins: string[]
agreementSigned: boolean
status: "pending" | "evaluating" | "approved" | "active" | "redeemed" | "overdue" | "forfeited"
approvedAt?: string
redeemedAt?: string
expiresAt?: string
createdAt: string
updatedAt: string
}
export interface AccountTransaction {
id?: number
orderNo: string
buyerId: number
sellerId: number
accountId: number
price: number
platformFee: number
sellerIncome: number
status: "pending" | "paid" | "transferring" | "completed" | "disputed" | "refunded"
createdAt: string
completedAt?: string
}
// ==================== 电竞酒店/网咖 ====================
export interface EsportsVenue {
id?: number
name: string
type: "hotel" | "cafe"
logo: string
images: string[]
description: string
address: string
city: string
district: string
latitude: number
longitude: number
phone: string
rating: number
reviewCount: number
pricePerHour?: number // 网咖按小时
pricePerNight?: number // 酒店按晚
facilities: string[]
tags: string[]
openTime: string
closeTime: string
isOpen24Hours: boolean
status: "active" | "closed" | "renovating"
createdAt: string
updatedAt: string
}
export interface VenueRoom {
id?: number
venueId: number
name: string
type: "single" | "double" | "vip" | "booth"
capacity: number
price: number
images: string[]
facilities: string[]
status: "available" | "occupied" | "maintenance"
}
export interface VenueBooking {
id?: number
orderNo: string
userId: number
venueId: number
roomId?: number
type: "hotel" | "cafe"
checkInTime: string
checkOutTime: string
duration: number // 小时
price: number
totalAmount: number
guests: number
contactName: string
contactPhone: string
status: "pending" | "confirmed" | "checked_in" | "completed" | "cancelled"
createdAt: string
confirmedAt?: string
}
// ==================== 钱包/交易 ====================
export interface Transaction {
id?: number
userId: number
orderNo: string
type: "recharge" | "withdraw" | "purchase" | "gift" | "income" | "refund" | "transfer"
category: string // 具体分类
amount: number
balanceBefore: number
balanceAfter: number
relatedId?: number // 关联订单ID
relatedType?: string // 关联类型
description: string
status: "pending" | "completed" | "failed"
createdAt: string
}
export interface RechargeOrder {
id?: number
orderNo: string
userId: number
amount: number
bonusAmount: number // 赠送金额
totalAmount: number
paymentMethod: "wechat" | "alipay" | "card"
status: "pending" | "paid" | "failed" | "refunded"
paidAt?: string
createdAt: string
}
export interface WithdrawOrder {
id?: number
orderNo: string
userId: number
amount: number
fee: number
actualAmount: number
accountType: "wechat" | "alipay" | "bank"
accountInfo: string
status: "pending" | "processing" | "completed" | "rejected"
reviewedBy?: number
reviewedAt?: string
completedAt?: string
rejectReason?: string
createdAt: string
}
// ==================== 消息/通知 ====================
export interface Message {
id?: number
conversationId: number
senderId: number
receiverId: number
type: "text" | "image" | "voice" | "gift" | "system"
content: string
mediaUrl?: string
duration?: number // 语音时长
isRead: boolean
createdAt: string
}
export interface Conversation {
id?: number
type: "private" | "group" | "system"
participants: number[]
lastMessage?: string
lastMessageTime?: string
unreadCount: number
isPinned: boolean
isMuted: boolean
createdAt: string
updatedAt: string
}
export interface Notification {
id?: number
userId: number
type: "system" | "order" | "follow" | "like" | "comment" | "gift" | "activity"
title: string
content: string
image?: string
link?: string
isRead: boolean
createdAt: string
}
// ==================== 动态/社交 ====================
export interface Moment {
id?: number
userId: number
userName: string
userAvatar: string
content: string
images: string[]
video?: string
game?: string
tags: string[]
location?: string
likes: number
comments: number
shares: number
isLiked: boolean
visibility: "public" | "followers" | "private"
status: "active" | "deleted" | "hidden"
createdAt: string
updatedAt: string
}
export interface MomentLike {
id?: number
momentId: number
userId: number
createdAt: string
}
export interface MomentComment {
id?: number
momentId: number
userId: number
userName: string
userAvatar: string
content: string
replyTo?: number // 回复的评论ID
replyToUser?: string
likes: number
status: "active" | "deleted"
createdAt: string
}
// ==================== 游戏数据 ====================
export interface Game {
id?: number
name: string
shortName: string
logo: string
coverImage: string
category: "moba" | "fps" | "mmorpg" | "card" | "sports" | "other"
platform: ("pc" | "mobile" | "console")[]
publisher: string
isHot: boolean
sortOrder: number
status: "active" | "inactive"
createdAt: string
}
export interface GameServer {
id?: number
gameId: number
name: string
region: string
sortOrder: number
status: "active" | "inactive"
}
// ==================== 数据库类定义 ====================
export class WanzhiDatabase extends Dexie {
// 用户相关
users!: Table<User>
userAuths!: Table<UserAuth>
userFollows!: Table<UserFollow>
// 主播相关
streamers!: Table<Streamer>
streamerCourses!: Table<StreamerCourse>
coursePurchases!: Table<CoursePurchase>
// 公会相关
guilds!: Table<Guild>
guildMembers!: Table<GuildMember>
guildApplications!: Table<GuildApplication>
// 明星相关
stars!: Table<Star>
starPartyRooms!: Table<StarPartyRoom>
starPartyMembers!: Table<StarPartyMember>
// CP/陪练
companions!: Table<Companion>
companionOrders!: Table<CompanionOrder>
// 星球匹配
planetMatches!: Table<PlanetMatch>
// 直播相关
liveRooms!: Table<LiveRoom>
liveGifts!: Table<LiveGift>
liveComments!: Table<LiveComment>
// 商城相关
products!: Table<Product>
cartItems!: Table<CartItem>
productOrders!: Table<ProductOrder>
// 游戏点卡
gamePointCards!: Table<GamePointCard>
pointCardOrders!: Table<PointCardOrder>
// 账号交易/典当
gameAccounts!: Table<GameAccount>
accountPawns!: Table<AccountPawn>
accountTransactions!: Table<AccountTransaction>
// 电竞酒店/网咖
esportsVenues!: Table<EsportsVenue>
venueRooms!: Table<VenueRoom>
venueBookings!: Table<VenueBooking>
// 钱包/交易
transactions!: Table<Transaction>
rechargeOrders!: Table<RechargeOrder>
withdrawOrders!: Table<WithdrawOrder>
// 消息/通知
messages!: Table<Message>
conversations!: Table<Conversation>
notifications!: Table<Notification>
// 动态/社交
moments!: Table<Moment>
momentLikes!: Table<MomentLike>
momentComments!: Table<MomentComment>
// 游戏数据
games!: Table<Game>
gameServers!: Table<GameServer>
constructor() {
super("WanzhiEsportsDB")
this.version(1).stores({
// 用户相关
users: "++id, odlId, phone, name, status, createdAt",
userAuths: "++id, userId, authType, authId",
userFollows: "++id, userId, followUserId, followType",
// 主播相关
streamers: "++id, userId, name, game, guildId, status, isLive",
streamerCourses: "++id, streamerId, game, status, price",
coursePurchases: "++id, userId, courseId, streamerId",
// 公会相关
guilds: "++id, name, ownerId, status",
guildMembers: "++id, guildId, userId, role, status",
guildApplications: "++id, guildId, userId, status",
// 明星相关
stars: "++id, userId, name, game, status",
starPartyRooms: "++id, starId, isActive",
starPartyMembers: "++id, partyRoomId, userId",
// CP/陪练
companions: "++id, userId, category, gender, status, onlineStatus",
companionOrders: "++id, orderNo, userId, companionId, status",
// 星球匹配
planetMatches: "++id, userId, matchType, status",
// 直播相关
liveRooms: "++id, streamerId, game, status",
liveGifts: "++id, liveRoomId, userId",
liveComments: "++id, liveRoomId, userId",
// 商城相关
products: "++id, name, category, game, status",
cartItems: "++id, userId, productId",
productOrders: "++id, orderNo, userId, status",
// 游戏点卡
gamePointCards: "++id, game, gameId, status",
pointCardOrders: "++id, orderNo, userId, status",
// 账号交易/典当
gameAccounts: "++id, sellerId, game, status",
accountPawns: "++id, userId, accountId, status",
accountTransactions: "++id, orderNo, buyerId, sellerId, status",
// 电竞酒店/网咖
esportsVenues: "++id, name, type, city, status",
venueRooms: "++id, venueId, type, status",
venueBookings: "++id, orderNo, userId, venueId, status",
// 钱包/交易
transactions: "++id, userId, orderNo, type, status",
rechargeOrders: "++id, orderNo, userId, status",
withdrawOrders: "++id, orderNo, userId, status",
// 消息/通知
messages: "++id, conversationId, senderId, receiverId, isRead",
conversations: "++id, type, updatedAt",
notifications: "++id, userId, type, isRead",
// 动态/社交
moments: "++id, userId, game, status, createdAt",
momentLikes: "++id, momentId, userId",
momentComments: "++id, momentId, userId",
// 游戏数据
games: "++id, name, category, isHot, sortOrder",
gameServers: "++id, gameId, region",
})
}
}
// 创建数据库实例
export const db = new WanzhiDatabase()

750
lib/db/seed.ts Normal file
View File

@@ -0,0 +1,750 @@
// 数据库初始化种子数据
import { db } from "./schema"
import type { User, Game, Streamer, Star, Guild, Companion, EsportsVenue, Product, GamePointCard } from "./schema"
export async function seedDatabase() {
// 检查是否已初始化
const existingUsers = await db.users.count()
if (existingUsers > 0) {
console.log("[v0] Database already seeded")
return
}
console.log("[v0] Seeding database...")
// 1. 初始化游戏数据
const games: Omit<Game, "id">[] = [
{
name: "王者荣耀",
shortName: "wzry",
logo: "/wzry.jpg",
coverImage: "/wzry-cover.jpg",
category: "moba",
platform: ["mobile"],
publisher: "腾讯",
isHot: true,
sortOrder: 1,
status: "active",
createdAt: new Date().toISOString(),
},
{
name: "英雄联盟",
shortName: "lol",
logo: "/lol.jpg",
coverImage: "/lol-cover.jpg",
category: "moba",
platform: ["pc"],
publisher: "腾讯",
isHot: true,
sortOrder: 2,
status: "active",
createdAt: new Date().toISOString(),
},
{
name: "和平精英",
shortName: "pubgm",
logo: "/pubgm.jpg",
coverImage: "/pubgm-cover.jpg",
category: "fps",
platform: ["mobile"],
publisher: "腾讯",
isHot: true,
sortOrder: 3,
status: "active",
createdAt: new Date().toISOString(),
},
{
name: "原神",
shortName: "ys",
logo: "/ys.jpg",
coverImage: "/ys-cover.jpg",
category: "mmorpg",
platform: ["pc", "mobile"],
publisher: "米哈游",
isHot: true,
sortOrder: 4,
status: "active",
createdAt: new Date().toISOString(),
},
{
name: "无畏契约",
shortName: "valorant",
logo: "/valorant.jpg",
coverImage: "/valorant-cover.jpg",
category: "fps",
platform: ["pc"],
publisher: "拳头",
isHot: true,
sortOrder: 5,
status: "active",
createdAt: new Date().toISOString(),
},
{
name: "穿越火线",
shortName: "cf",
logo: "/cf.jpg",
coverImage: "/cf-cover.jpg",
category: "fps",
platform: ["pc", "mobile"],
publisher: "腾讯",
isHot: false,
sortOrder: 6,
status: "active",
createdAt: new Date().toISOString(),
},
{
name: "梦幻西游",
shortName: "mhxy",
logo: "/mhxy.jpg",
coverImage: "/mhxy-cover.jpg",
category: "mmorpg",
platform: ["pc", "mobile"],
publisher: "网易",
isHot: false,
sortOrder: 7,
status: "active",
createdAt: new Date().toISOString(),
},
{
name: "三角洲行动",
shortName: "delta",
logo: "/delta.jpg",
coverImage: "/delta-cover.jpg",
category: "fps",
platform: ["pc", "mobile"],
publisher: "腾讯",
isHot: true,
sortOrder: 8,
status: "active",
createdAt: new Date().toISOString(),
},
]
await db.games.bulkAdd(games)
// 2. 初始化默认用户
const defaultUser: Omit<User, "id"> = {
odlId: "888888",
phone: "13800138000",
name: "卡若",
avatar: "/gamer-boy-esports-jersey.jpg",
level: 12,
experience: 2450,
isVip: false,
gender: "male",
tags: ["英雄联盟", "王者荣耀"],
activity: 128,
following: 45,
followers: 892,
visitors: 1205,
balance: 6880,
totalRecharge: 10000,
totalSpent: 3120,
registerSource: "app",
registerTime: "2023-01-15",
lastLoginTime: new Date().toISOString(),
status: "active",
createdAt: "2023-01-15",
updatedAt: new Date().toISOString(),
}
await db.users.add(defaultUser)
// 3. 初始化主播数据
const streamers: Omit<Streamer, "id">[] = [
{
userId: 1,
name: "GM-远洋",
avatar: "/streamer-1.jpg",
coverImage: "/lol-stream.jpg",
title: "国服第一盲僧",
game: "英雄联盟",
gameId: 2,
tags: ["打野", "教学"],
level: 28,
fans: 1256000,
hotValue: 125.6,
isLive: true,
totalGifts: 89000,
totalIncome: 156000,
commissionRate: 0.7,
status: "approved",
verifiedAt: "2023-02-01",
createdAt: "2023-01-20",
updatedAt: new Date().toISOString(),
},
{
userId: 2,
name: "小狐狸",
avatar: "/streamer-2.jpg",
coverImage: "/valorant-stream.jpg",
title: "无畏契约电竞女神",
game: "无畏契约",
gameId: 5,
tags: ["竞技", "颜值"],
level: 25,
fans: 856000,
hotValue: 89.2,
isLive: true,
totalGifts: 67000,
totalIncome: 98000,
commissionRate: 0.7,
status: "approved",
verifiedAt: "2023-03-01",
createdAt: "2023-02-15",
updatedAt: new Date().toISOString(),
},
{
userId: 3,
name: "王者阿杰",
avatar: "/streamer-3.jpg",
coverImage: "/wzry-stream.jpg",
title: "国服最强边路",
game: "王者荣耀",
gameId: 1,
tags: ["边路", "上分"],
level: 30,
fans: 2100000,
hotValue: 168.5,
isLive: false,
totalGifts: 125000,
totalIncome: 230000,
commissionRate: 0.75,
status: "approved",
verifiedAt: "2023-01-10",
createdAt: "2022-12-01",
updatedAt: new Date().toISOString(),
},
{
userId: 4,
name: "原神小可爱",
avatar: "/streamer-4.jpg",
coverImage: "/ys-stream.jpg",
title: "深渊满星攻略",
game: "原神",
gameId: 4,
tags: ["攻略", "抽卡"],
level: 22,
fans: 568000,
hotValue: 56.8,
isLive: true,
totalGifts: 45000,
totalIncome: 78000,
commissionRate: 0.65,
status: "approved",
verifiedAt: "2023-04-01",
createdAt: "2023-03-10",
updatedAt: new Date().toISOString(),
},
{
userId: 5,
name: "吃鸡大魔王",
avatar: "/streamer-5.jpg",
coverImage: "/pubgm-stream.jpg",
title: "单排上分王",
game: "和平精英",
gameId: 3,
tags: ["吃鸡", "技巧"],
level: 26,
fans: 980000,
hotValue: 98.0,
isLive: true,
totalGifts: 72000,
totalIncome: 135000,
commissionRate: 0.7,
status: "approved",
verifiedAt: "2023-02-15",
createdAt: "2023-01-25",
updatedAt: new Date().toISOString(),
},
{
userId: 6,
name: "魔兽老玩家",
avatar: "/streamer-6.jpg",
coverImage: "/wow-stream.jpg",
title: "怀旧服金团领队",
game: "魔兽世界",
gameId: 9,
tags: ["怀旧", "金团"],
level: 20,
fans: 320000,
hotValue: 32.5,
isLive: false,
totalGifts: 28000,
totalIncome: 52000,
commissionRate: 0.65,
status: "approved",
verifiedAt: "2023-05-01",
createdAt: "2023-04-20",
updatedAt: new Date().toISOString(),
},
]
await db.streamers.bulkAdd(streamers)
// 4. 初始化明星数据
const stars: Omit<Star, "id">[] = [
{
userId: 10,
name: "Faker",
avatar: "/star-faker.jpg",
coverImage: "/star-faker-cover.jpg",
title: "LOL传奇中单",
game: "英雄联盟",
gameId: 2,
tags: ["世界冠军", "中单"],
fans: 46800000,
hotValue: 468.0,
achievements: ["三冠王", "S赛MVP"],
teamName: "T1",
socialLinks: { weibo: "faker_lol" },
partyRoomId: "faker-party",
hourlyRate: 9999,
isOnline: true,
status: "active",
verifiedAt: "2022-01-01",
createdAt: "2022-01-01",
updatedAt: new Date().toISOString(),
},
{
userId: 11,
name: "Uzi",
avatar: "/star-uzi.jpg",
coverImage: "/star-uzi-cover.jpg",
title: "永远的神",
game: "英雄联盟",
gameId: 2,
tags: ["ADC", "传奇"],
fans: 38500000,
hotValue: 385.0,
achievements: ["MSI冠军", "LPL冠军"],
teamName: "RNG",
socialLinks: { weibo: "uzi_lol", douyin: "uzi" },
partyRoomId: "uzi-party",
hourlyRate: 8888,
isOnline: false,
status: "active",
verifiedAt: "2022-01-01",
createdAt: "2022-01-01",
updatedAt: new Date().toISOString(),
},
{
userId: 12,
name: "梦泪",
avatar: "/star-menglei.jpg",
coverImage: "/star-menglei-cover.jpg",
title: "国服第一韩信",
game: "王者荣耀",
gameId: 1,
tags: ["韩信", "打野"],
fans: 52000000,
hotValue: 520.0,
achievements: ["KPL冠军", "最佳打野"],
teamName: "AG超玩会",
socialLinks: { douyin: "menglei" },
partyRoomId: "menglei-party",
hourlyRate: 6666,
isOnline: true,
status: "active",
verifiedAt: "2022-06-01",
createdAt: "2022-06-01",
updatedAt: new Date().toISOString(),
},
{
userId: 13,
name: "不求人",
avatar: "/star-buqiuren.jpg",
coverImage: "/star-buqiuren-cover.jpg",
title: "和平精英一哥",
game: "和平精英",
gameId: 3,
tags: ["吃鸡", "主播"],
fans: 41000000,
hotValue: 410.0,
achievements: ["PEL冠军", "全明星MVP"],
socialLinks: { douyin: "buqiuren" },
partyRoomId: "buqiuren-party",
hourlyRate: 5888,
isOnline: true,
status: "active",
verifiedAt: "2022-03-01",
createdAt: "2022-03-01",
updatedAt: new Date().toISOString(),
},
]
await db.stars.bulkAdd(stars)
// 5. 初始化公会数据
const guilds: Omit<Guild, "id">[] = [
{
name: "星耀电竞",
logo: "/guild-xingyao.jpg",
coverImage: "/guild-xingyao-cover.jpg",
description: "顶级电竞公会,培养众多职业选手",
ownerId: 1,
memberCount: 1280,
maxMembers: 2000,
level: 8,
totalIncome: 2580000,
commissionRate: 0.1,
tags: ["职业", "培训"],
games: ["英雄联盟", "王者荣耀"],
requirements: "段位王者以上",
benefits: ["签约底薪", "流量扶持", "赛事推荐"],
status: "active",
createdAt: "2022-01-01",
updatedAt: new Date().toISOString(),
},
{
name: "凤凰涅槃",
logo: "/guild-fenghuang.jpg",
coverImage: "/guild-fenghuang-cover.jpg",
description: "女主播孵化基地",
ownerId: 2,
memberCount: 860,
maxMembers: 1500,
level: 6,
totalIncome: 1680000,
commissionRate: 0.12,
tags: ["颜值", "才艺"],
games: ["王者荣耀", "和平精英"],
requirements: "颜值在线,有才艺",
benefits: ["形象包装", "运营指导", "商务对接"],
status: "active",
createdAt: "2022-06-01",
updatedAt: new Date().toISOString(),
},
{
name: "暗影战队",
logo: "/guild-anying.jpg",
coverImage: "/guild-anying-cover.jpg",
description: "FPS游戏专业公会",
ownerId: 3,
memberCount: 520,
maxMembers: 1000,
level: 5,
totalIncome: 980000,
commissionRate: 0.1,
tags: ["FPS", "硬核"],
games: ["和平精英", "无畏契约", "穿越火线"],
requirements: "FPS游戏高段位",
benefits: ["战队训练", "比赛机会", "装备赞助"],
status: "active",
createdAt: "2023-01-01",
updatedAt: new Date().toISOString(),
},
]
await db.guilds.bulkAdd(guilds)
// 6. 初始化CP/陪练数据
const companions: Omit<Companion, "id">[] = [
{
userId: 20,
name: "甜心小姐姐",
avatar: "/cp-avatar-1.jpg",
coverImage: "/cp-cover-1.jpg",
gender: "female",
age: 22,
tags: ["甜美", "温柔", "会撒娇"],
games: ["王者荣耀", "和平精英"],
bio: "声音甜美,陪你上分更开心~",
price: 30,
originalPrice: 50,
rating: 4.9,
ordersCount: 1256,
responseRate: 98,
onlineStatus: "online",
category: "cp",
commissionRate: 0.3,
status: "active",
createdAt: "2023-01-01",
updatedAt: new Date().toISOString(),
},
{
userId: 21,
name: "电竞小王子",
avatar: "/cp-avatar-2.jpg",
coverImage: "/cp-cover-2.jpg",
gender: "male",
age: 24,
tags: ["阳光", "幽默", "技术好"],
games: ["英雄联盟", "无畏契约"],
bio: "王者段位,带你轻松上分",
price: 35,
originalPrice: 60,
rating: 4.8,
ordersCount: 986,
responseRate: 95,
onlineStatus: "online",
category: "cp",
commissionRate: 0.3,
status: "active",
createdAt: "2023-02-01",
updatedAt: new Date().toISOString(),
},
{
userId: 22,
name: "职业选手小明",
avatar: "/coach-avatar-1.jpg",
coverImage: "/coach-cover-1.jpg",
gender: "male",
age: 26,
tags: ["职业", "教学", "耐心"],
games: ["英雄联盟"],
voiceSample: "/voice-sample-1.mp3",
bio: "前职业选手,专业教学",
price: 100,
originalPrice: 150,
rating: 4.95,
ordersCount: 568,
responseRate: 99,
onlineStatus: "online",
category: "coach",
coachType: "pro",
rank: "王者",
commissionRate: 0.25,
status: "active",
createdAt: "2023-01-15",
updatedAt: new Date().toISOString(),
},
{
userId: 23,
name: "王者大神",
avatar: "/coach-avatar-2.jpg",
coverImage: "/coach-cover-2.jpg",
gender: "male",
age: 23,
tags: ["上分", "技术流", "细节"],
games: ["王者荣耀"],
bio: "国服百星,带你冲击荣耀王者",
price: 80,
originalPrice: 120,
rating: 4.88,
ordersCount: 892,
responseRate: 97,
onlineStatus: "busy",
category: "coach",
coachType: "master",
rank: "荣耀王者100星",
commissionRate: 0.25,
status: "active",
createdAt: "2023-03-01",
updatedAt: new Date().toISOString(),
},
]
await db.companions.bulkAdd(companions)
// 7. 初始化电竞酒店/网咖数据
const venues: Omit<EsportsVenue, "id">[] = [
{
name: "玩值电竞酒店(旗舰店)",
type: "hotel",
logo: "/hotel-logo-1.jpg",
images: ["/hotel-1.jpg"],
description: "五星级电竞酒店体验",
address: "厦门市思明区软件园二期",
city: "厦门",
district: "思明区",
latitude: 24.4798,
longitude: 118.0894,
phone: "0592-12345678",
rating: 4.9,
reviewCount: 1256,
pricePerNight: 399,
facilities: ["RTX4090", "电竞椅", "144Hz显示器", "独立卫浴"],
tags: ["热门", "高端"],
openTime: "00:00",
closeTime: "24:00",
isOpen24Hours: true,
status: "active",
createdAt: "2022-01-01",
updatedAt: new Date().toISOString(),
},
{
name: "极客空间电竞公寓",
type: "hotel",
logo: "/hotel-logo-2.jpg",
images: ["/hotel-2.jpg"],
description: "年轻人的电竞乐园",
address: "厦门市湖里区五缘湾",
city: "厦门",
district: "湖里区",
latitude: 24.5102,
longitude: 118.1456,
phone: "0592-23456789",
rating: 4.7,
reviewCount: 896,
pricePerNight: 299,
facilities: ["RTX3080", "电竞椅", "双人房"],
tags: ["性价比"],
openTime: "00:00",
closeTime: "24:00",
isOpen24Hours: true,
status: "active",
createdAt: "2022-06-01",
updatedAt: new Date().toISOString(),
},
{
name: "网鱼网咖(万达店)",
type: "cafe",
logo: "/cafe-logo-1.jpg",
images: ["/cafe-1.jpg"],
description: "专业电竞网咖",
address: "厦门市思明区万达广场",
city: "厦门",
district: "思明区",
latitude: 24.4656,
longitude: 118.1023,
phone: "0592-34567890",
rating: 4.8,
reviewCount: 2356,
pricePerHour: 12,
facilities: ["3080显卡", "特权公馆", "现磨咖啡"],
tags: ["连锁", "环境好"],
openTime: "00:00",
closeTime: "24:00",
isOpen24Hours: true,
status: "active",
createdAt: "2021-01-01",
updatedAt: new Date().toISOString(),
},
{
name: "杰拉电竞馆",
type: "cafe",
logo: "/cafe-logo-2.jpg",
images: ["/cafe-2.jpg"],
description: "赛事级电竞馆",
address: "厦门市集美区银泰城",
city: "厦门",
district: "集美区",
latitude: 24.5789,
longitude: 118.0956,
phone: "0592-45678901",
rating: 4.6,
reviewCount: 1568,
pricePerHour: 10,
facilities: ["赛事级配置", "美女陪玩", "独立包间"],
tags: ["比赛场地"],
openTime: "09:00",
closeTime: "02:00",
isOpen24Hours: false,
status: "active",
createdAt: "2022-03-01",
updatedAt: new Date().toISOString(),
},
]
await db.esportsVenues.bulkAdd(venues)
// 8. 初始化商品数据
const products: Omit<Product, "id">[] = [
{
name: "王者荣耀皮肤礼包",
description: "稀有皮肤随机礼包",
image: "/product-skin-1.jpg",
images: ["/product-skin-1.jpg"],
category: "skin",
game: "王者荣耀",
price: 199,
originalPrice: 299,
stock: 1000,
sales: 5680,
rating: 4.8,
tags: ["热销", "限定"],
status: "active",
createdAt: "2023-01-01",
updatedAt: new Date().toISOString(),
},
{
name: "电竞机械键盘",
description: "青轴机械键盘RGB背光",
image: "/product-keyboard.jpg",
images: ["/product-keyboard.jpg"],
category: "peripheral",
price: 299,
originalPrice: 399,
stock: 500,
sales: 2356,
rating: 4.9,
tags: ["外设", "推荐"],
status: "active",
createdAt: "2023-02-01",
updatedAt: new Date().toISOString(),
},
{
name: "游戏鼠标垫",
description: "超大电竞鼠标垫",
image: "/product-mousepad.jpg",
images: ["/product-mousepad.jpg"],
category: "peripheral",
price: 59,
originalPrice: 99,
stock: 2000,
sales: 8956,
rating: 4.7,
tags: ["热销"],
status: "active",
createdAt: "2023-03-01",
updatedAt: new Date().toISOString(),
},
]
await db.products.bulkAdd(products)
// 9. 初始化游戏点卡数据
const pointCards: Omit<GamePointCard, "id">[] = [
{
game: "王者荣耀",
gameId: 1,
gameLogo: "/wzry.jpg",
name: "点券充值",
description: "官方点券直充",
faceValue: 100,
price: 98,
discount: 0.98,
stock: 9999,
sales: 12568,
deliveryType: "instant",
instructions: "请提供游戏账号",
status: "active",
createdAt: "2023-01-01",
},
{
game: "英雄联盟",
gameId: 2,
gameLogo: "/lol.jpg",
name: "点券充值",
description: "官方点券直充",
faceValue: 100,
price: 97,
discount: 0.97,
stock: 9999,
sales: 9856,
deliveryType: "instant",
instructions: "请提供游戏账号",
status: "active",
createdAt: "2023-01-01",
},
{
game: "原神",
gameId: 4,
gameLogo: "/ys.jpg",
name: "创世结晶",
description: "官方充值",
faceValue: 648,
price: 628,
discount: 0.97,
stock: 9999,
sales: 15689,
deliveryType: "instant",
instructions: "请提供UID",
status: "active",
createdAt: "2023-01-01",
},
]
await db.gamePointCards.bulkAdd(pointCards)
console.log("[v0] Database seeded successfully!")
}
// 清空数据库
export async function clearDatabase() {
await db.delete()
await db.open()
console.log("[v0] Database cleared")
}

View File

@@ -45,6 +45,7 @@
"cmdk": "1.0.4",
"crypto": "1.0.1",
"date-fns": "4.1.0",
"dexie": "4.2.1",
"embla-carousel-react": "8.5.1",
"framer-motion": "12.23.24",
"input-otp": "1.4.1",

8
pnpm-lock.yaml generated
View File

@@ -116,6 +116,9 @@ importers:
date-fns:
specifier: 4.1.0
version: 4.1.0
dexie:
specifier: 4.2.1
version: 4.2.1
embla-carousel-react:
specifier: 8.5.1
version: 8.5.1(react@19.2.0)
@@ -1348,6 +1351,9 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dexie@4.2.1:
resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==}
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
@@ -2880,6 +2886,8 @@ snapshots:
detect-node-es@1.1.0: {}
dexie@4.2.1: {}
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.28.4

BIN
public/admin-avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 153 KiB

BIN
public/party-hok.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
public/party-lol.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
public/party-team.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 199 KiB

BIN
public/wanzhi-logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

17
public/wanzhi-logo.svg Normal file
View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#14b8a6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:1" />
</linearGradient>
</defs>
<circle cx="50" cy="50" r="45" fill="none" stroke="url(#grad1)" stroke-width="3"/>
<path d="M25 45 Q25 35 35 35 L40 35 L40 30 Q40 25 50 25 Q60 25 60 30 L60 35 L65 35 Q75 35 75 45 L75 55 Q75 70 60 70 L55 60 L45 60 L40 70 Q25 70 25 55 Z"
fill="url(#grad1)" opacity="0.9"/>
<circle cx="35" cy="48" r="6" fill="#0d1117"/>
<circle cx="62" cy="45" r="3" fill="#0d1117"/>
<circle cx="68" cy="51" r="3" fill="#0d1117"/>
<rect x="48" y="40" width="4" height="12" fill="#0d1117" rx="1"/>
<rect x="44" y="44" width="12" height="4" fill="#0d1117" rx="1"/>
<text x="50" y="88" font-family="Arial Black, sans-serif" font-size="16" font-weight="bold" fill="url(#grad1)" text-anchor="middle">W</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB