Add new files
#VERCEL_SKIP Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
70
app/account-help/page.tsx
Normal 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
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
280
app/admin/mall/client.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
app/admin/mall/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
7
app/admin/mall/page.tsx
Normal 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
@@ -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>
|
||||
)
|
||||
}
|
||||
3
app/admin/orders/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
7
app/admin/orders/page.tsx
Normal 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
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
3
app/admin/pawn/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
7
app/admin/pawn/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const AdminPawnClient = dynamic(() => import("./client"), { ssr: false })
|
||||
|
||||
export default function AdminPawnPage() {
|
||||
return <AdminPawnClient />
|
||||
}
|
||||
3
app/admin/settings/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
300
app/admin/settings/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
313
app/admin/streamers/client.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
app/admin/streamers/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
7
app/admin/streamers/page.tsx
Normal 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
@@ -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>
|
||||
)
|
||||
}
|
||||
3
app/admin/users/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
7
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const AdminUsersClient = dynamic(() => import("./client"), { ssr: false })
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
return <AdminUsersClient />
|
||||
}
|
||||
144
app/customer-service/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
10
app/party/[id]/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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">享网咖8折、酒店9折特权</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
97
app/transaction-history/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
63
components/providers/database-provider.tsx
Normal 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
@@ -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
@@ -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 }
|
||||
257
components/ui/dropdown-menu.tsx
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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
|
After Width: | Height: | Size: 686 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 153 KiB |
BIN
public/party-hok.jpg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
public/party-lol.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
public/party-team.jpg
Normal file
|
After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 199 KiB |
BIN
public/wanzhi-logo.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
17
public/wanzhi-logo.svg
Normal 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 |