Initial commit to prepare for remote overwrite
This commit is contained in:
3
app/admin/content/loading.tsx
Normal file
3
app/admin/content/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
335
app/admin/content/page.tsx
Normal file
335
app/admin/content/page.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { bookData } from "@/lib/book-data"
|
||||
import {
|
||||
FileText,
|
||||
BookOpen,
|
||||
Settings2,
|
||||
ChevronRight,
|
||||
CheckCircle,
|
||||
Edit3,
|
||||
Save,
|
||||
X,
|
||||
RefreshCw,
|
||||
Link2,
|
||||
} from "lucide-react"
|
||||
|
||||
interface EditingSection {
|
||||
id: string
|
||||
title: string
|
||||
price: number
|
||||
content?: string
|
||||
}
|
||||
|
||||
export default function ContentPage() {
|
||||
const [expandedParts, setExpandedParts] = useState<string[]>(["part-1"])
|
||||
const [editingSection, setEditingSection] = useState<EditingSection | null>(null)
|
||||
const [isSyncing, setIsSyncing] = useState(false)
|
||||
const [feishuDocUrl, setFeishuDocUrl] = useState("")
|
||||
const [showFeishuModal, setShowFeishuModal] = useState(false)
|
||||
|
||||
const togglePart = (partId: string) => {
|
||||
setExpandedParts((prev) => (prev.includes(partId) ? prev.filter((id) => id !== partId) : [...prev, partId]))
|
||||
}
|
||||
|
||||
const totalSections = bookData.reduce(
|
||||
(sum, part) => sum + part.chapters.reduce((cSum, ch) => cSum + ch.sections.length, 0),
|
||||
0,
|
||||
)
|
||||
|
||||
const handleEditSection = (section: { id: string; title: string; price: number }) => {
|
||||
setEditingSection({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
content: "",
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveSection = () => {
|
||||
if (editingSection) {
|
||||
// 保存到本地存储或API
|
||||
console.log("[v0] Saving section:", editingSection)
|
||||
alert(`已保存章节: ${editingSection.title}`)
|
||||
setEditingSection(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSyncFeishu = async () => {
|
||||
if (!feishuDocUrl) {
|
||||
alert("请输入飞书文档链接")
|
||||
return
|
||||
}
|
||||
setIsSyncing(true)
|
||||
// 模拟同步过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
setIsSyncing(false)
|
||||
setShowFeishuModal(false)
|
||||
alert("飞书文档同步成功!")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-6xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">内容管理</h2>
|
||||
<p className="text-gray-400 mt-1">
|
||||
共 {bookData.length} 篇 · {totalSections} 节内容
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowFeishuModal(true)} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
同步飞书文档
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 飞书同步弹窗 */}
|
||||
<Dialog open={showFeishuModal} onOpenChange={setShowFeishuModal}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Link2 className="w-5 h-5 text-[#38bdac]" />
|
||||
同步飞书文档
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">飞书文档链接</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
|
||||
placeholder="https://xxx.feishu.cn/docx/..."
|
||||
value={feishuDocUrl}
|
||||
onChange={(e) => setFeishuDocUrl(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">请确保文档已开启公开访问权限</p>
|
||||
</div>
|
||||
<div className="bg-[#38bdac]/10 border border-[#38bdac]/30 rounded-lg p-3">
|
||||
<p className="text-[#38bdac] text-sm">
|
||||
同步说明:系统将自动解析飞书文档结构,按照标题层级导入为章节内容。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowFeishuModal(false)}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSyncFeishu}
|
||||
disabled={isSyncing}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
{isSyncing ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
同步中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
开始同步
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 章节编辑弹窗 */}
|
||||
<Dialog open={!!editingSection} onOpenChange={() => setEditingSection(null)}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Edit3 className="w-5 h-5 text-[#38bdac]" />
|
||||
编辑章节
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{editingSection && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">章节标题</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={editingSection.title}
|
||||
onChange={(e) => setEditingSection({ ...editingSection, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">价格 (元)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0a1628] border-gray-700 text-white w-32"
|
||||
value={editingSection.price}
|
||||
onChange={(e) => setEditingSection({ ...editingSection, price: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">内容预览</Label>
|
||||
<Textarea
|
||||
className="bg-[#0a1628] border-gray-700 text-white min-h-[200px] placeholder:text-gray-500"
|
||||
placeholder="此处显示章节内容,支持Markdown格式..."
|
||||
value={editingSection.content}
|
||||
onChange={(e) => setEditingSection({ ...editingSection, content: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditingSection(null)}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSaveSection} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
保存修改
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Tabs defaultValue="chapters" className="space-y-6">
|
||||
<TabsList className="bg-[#0f2137] border border-gray-700/50 p-1">
|
||||
<TabsTrigger
|
||||
value="chapters"
|
||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400"
|
||||
>
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
章节管理
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="hooks"
|
||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 mr-2" />
|
||||
钩子配置
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="chapters" className="space-y-4">
|
||||
{bookData.map((part, partIndex) => (
|
||||
<Card key={part.id} className="bg-[#0f2137] border-gray-700/50 shadow-xl overflow-hidden">
|
||||
<CardHeader
|
||||
className="cursor-pointer hover:bg-[#162840] transition-colors"
|
||||
onClick={() => togglePart(part.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[#38bdac] font-mono text-sm">0{partIndex + 1}</span>
|
||||
<CardTitle className="text-white">{part.title}</CardTitle>
|
||||
<Badge variant="outline" className="text-gray-400 border-gray-600">
|
||||
{part.chapters.reduce((sum, ch) => sum + ch.sections.length, 0)} 节
|
||||
</Badge>
|
||||
</div>
|
||||
<ChevronRight
|
||||
className={`w-5 h-5 text-gray-400 transition-transform ${
|
||||
expandedParts.includes(part.id) ? "rotate-90" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{expandedParts.includes(part.id) && (
|
||||
<CardContent className="pt-0 pb-4">
|
||||
<div className="space-y-3 pl-8 border-l-2 border-gray-700">
|
||||
{part.chapters.map((chapter) => (
|
||||
<div key={chapter.id} className="space-y-2">
|
||||
<h4 className="font-medium text-gray-300">{chapter.title}</h4>
|
||||
<div className="space-y-1">
|
||||
{chapter.sections.map((section) => (
|
||||
<div
|
||||
key={section.id}
|
||||
className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-[#162840] text-sm group transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-[#38bdac]" />
|
||||
<span className="text-gray-400">{section.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[#38bdac] font-medium">
|
||||
{section.price === 0 ? "免费" : `¥${section.price}`}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditSection(section)}
|
||||
className="text-gray-500 hover:text-[#38bdac] hover:bg-[#38bdac]/10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Edit3 className="w-4 h-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hooks" className="space-y-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">引流钩子配置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor="hook-chapter" className="text-gray-300">
|
||||
触发章节
|
||||
</Label>
|
||||
<Select defaultValue="3">
|
||||
<SelectTrigger id="hook-chapter" className="bg-[#0a1628] border-gray-700 text-white">
|
||||
<SelectValue placeholder="选择章节" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#0f2137] border-gray-700">
|
||||
<SelectItem value="1" className="text-white hover:bg-[#38bdac]/20 focus:bg-[#38bdac]/20">
|
||||
第一章
|
||||
</SelectItem>
|
||||
<SelectItem value="2" className="text-white hover:bg-[#38bdac]/20 focus:bg-[#38bdac]/20">
|
||||
第二章
|
||||
</SelectItem>
|
||||
<SelectItem value="3" className="text-white hover:bg-[#38bdac]/20 focus:bg-[#38bdac]/20">
|
||||
第三章 (默认)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label htmlFor="message" className="text-gray-300">
|
||||
引流文案
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="输入引导用户加群的文案..."
|
||||
id="message"
|
||||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
|
||||
defaultValue="阅读更多精彩内容,请加入Soul创业实验派对群..."
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-[#38bdac] hover:bg-[#2da396] text-white">保存配置</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
77
app/admin/layout.tsx
Normal file
77
app/admin/layout.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { LayoutDashboard, FileText, Users, CreditCard, QrCode, Settings, LogOut, Wallet } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const { user, isLoggedIn } = useStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
// router.push("/my")
|
||||
}
|
||||
}, [isLoggedIn, router])
|
||||
|
||||
const menuItems = [
|
||||
{ icon: LayoutDashboard, label: "数据概览", href: "/admin" },
|
||||
{ icon: FileText, label: "内容管理", href: "/admin/content" },
|
||||
{ icon: Users, label: "用户管理", href: "/admin/users" },
|
||||
{ icon: CreditCard, label: "支付配置", href: "/admin/payment" },
|
||||
{ icon: Wallet, label: "提现管理", href: "/admin/withdrawals" },
|
||||
{ icon: QrCode, label: "二维码", href: "/admin/qrcodes" },
|
||||
{ icon: Settings, label: "系统设置", href: "/admin/settings" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0a1628]">
|
||||
{/* Sidebar - 深色侧边栏 */}
|
||||
<div className="w-64 bg-[#0f2137] flex flex-col border-r border-gray-700/50 shadow-xl">
|
||||
<div className="p-6 border-b border-gray-700/50">
|
||||
<h1 className="text-xl font-bold text-[#38bdac]">管理后台</h1>
|
||||
<p className="text-xs text-gray-400 mt-1">Soul创业实验场</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = pathname === item.href
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? "bg-[#38bdac]/20 text-[#38bdac] font-medium"
|
||||
: "text-gray-400 hover:bg-gray-700/50 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
<span className="text-sm">{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-700/50">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-400 hover:text-white rounded-lg hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="text-sm">返回前台</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content - 深色背景 */}
|
||||
<div className="flex-1 overflow-auto bg-[#0a1628]">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/admin/loading.tsx
Normal file
3
app/admin/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
3
app/admin/login/loading.tsx
Normal file
3
app/admin/login/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
111
app/admin/login/page.tsx
Normal file
111
app/admin/login/page.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Lock, User, ShieldCheck } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const router = useRouter()
|
||||
const { adminLogin } = useStore()
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError("")
|
||||
setLoading(true)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const success = adminLogin(username, password)
|
||||
if (success) {
|
||||
router.push("/admin")
|
||||
} else {
|
||||
setError("用户名或密码错误")
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a1628] flex items-center justify-center p-4">
|
||||
{/* 装饰背景 */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-[#38bdac]/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-[#38bdac]/20 rounded-2xl flex items-center justify-center mx-auto mb-4 border border-[#38bdac]/30">
|
||||
<ShieldCheck className="w-8 h-8 text-[#38bdac]" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">管理后台</h1>
|
||||
<p className="text-gray-400">一场SOUL的创业实验场</p>
|
||||
</div>
|
||||
|
||||
{/* Login form */}
|
||||
<div className="bg-[#0f2137] rounded-2xl p-8 shadow-xl border border-gray-700/50 backdrop-blur-xl">
|
||||
<h2 className="text-xl font-semibold text-white mb-6 text-center">管理员登录</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-400 text-sm mb-2">用户名</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<Input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="请输入用户名"
|
||||
className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 focus:border-[#38bdac]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-gray-400 text-sm mb-2">密码</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="请输入密码"
|
||||
className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 focus:border-[#38bdac]"
|
||||
onKeyDown={(e) => e.key === "Enter" && handleLogin()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 text-red-400 text-sm p-3 rounded-lg border border-red-500/20">{error}</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
className="w-full bg-[#38bdac] hover:bg-[#2da396] text-white py-5 disabled:opacity-50"
|
||||
>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-700/50">
|
||||
<p className="text-gray-500 text-xs text-center">
|
||||
默认账号: <span className="text-gray-300 font-mono">admin</span> /{" "}
|
||||
<span className="text-gray-300 font-mono">key123456</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-gray-500 text-xs mt-6">Soul创业实验场 · 后台管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
118
app/admin/page.tsx
Normal file
118
app/admin/page.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { Users, BookOpen, ShoppingBag, TrendingUp } from "lucide-react"
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { getAllUsers, getAllPurchases } = useStore()
|
||||
const users = getAllUsers()
|
||||
const purchases = getAllPurchases()
|
||||
|
||||
const totalRevenue = purchases.reduce((sum, p) => sum + p.amount, 0)
|
||||
const totalUsers = users.length
|
||||
const totalPurchases = purchases.length
|
||||
|
||||
const stats = [
|
||||
{ title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20" },
|
||||
{
|
||||
title: "总收入",
|
||||
value: `¥${totalRevenue.toFixed(2)}`,
|
||||
icon: TrendingUp,
|
||||
color: "text-[#38bdac]",
|
||||
bg: "bg-[#38bdac]/20",
|
||||
},
|
||||
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20" },
|
||||
{
|
||||
title: "转化率",
|
||||
value: `${totalUsers > 0 ? ((totalPurchases / totalUsers) * 100).toFixed(1) : 0}%`,
|
||||
icon: BookOpen,
|
||||
color: "text-orange-400",
|
||||
bg: "bg-orange-500/20",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-8 text-white">数据概览</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-400">{stat.title}</CardTitle>
|
||||
<div className={`p-2 rounded-lg ${stat.bg}`}>
|
||||
<stat.icon className={`w-4 h-4 ${stat.color}`} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">{stat.value}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">最近订单</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{purchases
|
||||
.slice(-5)
|
||||
.reverse()
|
||||
.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex items-center justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{p.sectionTitle || "整本购买"}</p>
|
||||
<p className="text-xs text-gray-500">{new Date(p.createdAt).toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-[#38bdac]">+¥{p.amount}</p>
|
||||
<p className="text-xs text-gray-400">{p.paymentMethod || "微信支付"}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{purchases.length === 0 && <p className="text-gray-500 text-center py-8">暂无订单数据</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">新注册用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{users
|
||||
.slice(-5)
|
||||
.reverse()
|
||||
.map((u) => (
|
||||
<div
|
||||
key={u.id}
|
||||
className="flex items-center justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
|
||||
{u.nickname.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{u.nickname}</p>
|
||||
<p className="text-xs text-gray-500">{u.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">{new Date(u.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
))}
|
||||
{users.length === 0 && <p className="text-gray-500 text-center py-8">暂无用户数据</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/admin/payment/loading.tsx
Normal file
3
app/admin/payment/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
375
app/admin/payment/page.tsx
Normal file
375
app/admin/payment/page.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Save,
|
||||
RefreshCw,
|
||||
Smartphone,
|
||||
CreditCard,
|
||||
ExternalLink,
|
||||
Bitcoin,
|
||||
Globe,
|
||||
Copy,
|
||||
Check,
|
||||
HelpCircle,
|
||||
} from "lucide-react"
|
||||
|
||||
export default function PaymentConfigPage() {
|
||||
const { settings, updateSettings, fetchSettings } = useStore()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [localSettings, setLocalSettings] = useState(settings.paymentMethods)
|
||||
const [copied, setCopied] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSettings(settings.paymentMethods)
|
||||
}, [settings.paymentMethods])
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true)
|
||||
updateSettings({ paymentMethods: localSettings })
|
||||
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||
setLoading(false)
|
||||
alert("配置已保存!")
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setLoading(true)
|
||||
await fetchSettings()
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleCopy = (text: string, field: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopied(field)
|
||||
setTimeout(() => setCopied(""), 2000)
|
||||
}
|
||||
|
||||
const updateWechat = (field: string, value: any) => {
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
wechat: { ...prev.wechat, [field]: value },
|
||||
}))
|
||||
}
|
||||
|
||||
const updateAlipay = (field: string, value: any) => {
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
alipay: { ...prev.alipay, [field]: value },
|
||||
}))
|
||||
}
|
||||
|
||||
const updateUsdt = (field: string, value: any) => {
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
usdt: { ...prev.usdt, [field]: value },
|
||||
}))
|
||||
}
|
||||
|
||||
const updatePaypal = (field: string, value: any) => {
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
paypal: { ...prev.paypal, [field]: value },
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-5xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2 text-white">支付配置</h1>
|
||||
<p className="text-gray-400">配置微信、支付宝、USDT、PayPal等支付参数</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRefresh}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? "animate-spin" : ""}`} />
|
||||
同步配置
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
保存配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 bg-[#07C160]/10 border border-[#07C160]/30 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<HelpCircle className="w-5 h-5 text-[#07C160] flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium mb-2 text-[#07C160]">如何获取微信群跳转链接?</p>
|
||||
<ol className="text-[#07C160]/80 space-y-1 list-decimal list-inside">
|
||||
<li>打开微信,进入目标微信群</li>
|
||||
<li>点击右上角"..." → "群二维码"</li>
|
||||
<li>点击右上角"..." → "发送到电脑"</li>
|
||||
<li>在电脑上保存二维码图片,上传到图床获取URL</li>
|
||||
<li>或使用草料二维码等工具解析二维码获取链接</li>
|
||||
</ol>
|
||||
<p className="text-[#07C160]/60 mt-2">提示:微信群二维码7天后失效,建议使用活码工具</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="wechat" className="space-y-6">
|
||||
<TabsList className="bg-[#0f2137] border border-gray-700/50 p-1 grid grid-cols-4 w-full">
|
||||
<TabsTrigger
|
||||
value="wechat"
|
||||
className="data-[state=active]:bg-[#07C160]/20 data-[state=active]:text-[#07C160] text-gray-400"
|
||||
>
|
||||
<Smartphone className="w-4 h-4 mr-2" />
|
||||
微信
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="alipay"
|
||||
className="data-[state=active]:bg-[#1677FF]/20 data-[state=active]:text-[#1677FF] text-gray-400"
|
||||
>
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
支付宝
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="usdt"
|
||||
className="data-[state=active]:bg-[#26A17B]/20 data-[state=active]:text-[#26A17B] text-gray-400"
|
||||
>
|
||||
<Bitcoin className="w-4 h-4 mr-2" />
|
||||
USDT
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="paypal"
|
||||
className="data-[state=active]:bg-[#003087]/20 data-[state=active]:text-[#169BD7] text-gray-400"
|
||||
>
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
PayPal
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 微信支付配置 */}
|
||||
<TabsContent value="wechat" className="space-y-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-[#07C160] flex items-center gap-2">
|
||||
<Smartphone className="w-5 h-5" />
|
||||
微信支付配置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置微信支付参数和跳转链接</CardDescription>
|
||||
</div>
|
||||
<Switch checked={localSettings.wechat.enabled} onCheckedChange={(c) => updateWechat("enabled", c)} />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* API配置 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">网站AppID</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white font-mono text-sm"
|
||||
value={localSettings.wechat.websiteAppId || ""}
|
||||
onChange={(e) => updateWechat("websiteAppId", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">商户号</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white font-mono text-sm"
|
||||
value={localSettings.wechat.merchantId || ""}
|
||||
onChange={(e) => updateWechat("merchantId", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 跳转链接配置 - 重点 */}
|
||||
<div className="border-t border-gray-700/50 pt-4 space-y-4">
|
||||
<h4 className="text-white font-medium flex items-center gap-2">
|
||||
<ExternalLink className="w-4 h-4 text-[#38bdac]" />
|
||||
跳转链接配置(核心功能)
|
||||
</h4>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">微信收款码/支付链接</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
|
||||
placeholder="https://收款码图片URL 或 weixin://支付链接"
|
||||
value={localSettings.wechat.qrCode || ""}
|
||||
onChange={(e) => updateWechat("qrCode", e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">用户点击微信支付后显示的二维码图片URL</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 bg-[#07C160]/5 p-4 rounded-xl border border-[#07C160]/20">
|
||||
<Label className="text-[#07C160] font-medium">微信群跳转链接(支付成功后跳转)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-[#07C160]/30 text-white placeholder:text-gray-500"
|
||||
placeholder="https://weixin.qq.com/g/... 或微信群二维码图片URL"
|
||||
value={localSettings.wechat.groupQrCode || ""}
|
||||
onChange={(e) => updateWechat("groupQrCode", e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-[#07C160]/70">用户支付成功后将自动跳转到此链接,进入指定微信群</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 支付宝配置 */}
|
||||
<TabsContent value="alipay" className="space-y-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-[#1677FF] flex items-center gap-2">
|
||||
<CreditCard className="w-5 h-5" />
|
||||
支付宝配置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">已加载真实支付宝参数</CardDescription>
|
||||
</div>
|
||||
<Switch checked={localSettings.alipay.enabled} onCheckedChange={(c) => updateAlipay("enabled", c)} />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">合作者身份 (PID)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white font-mono text-sm"
|
||||
value={localSettings.alipay.partnerId || ""}
|
||||
onChange={(e) => updateAlipay("partnerId", e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="border-gray-700 bg-transparent"
|
||||
onClick={() => handleCopy(localSettings.alipay.partnerId || "", "pid")}
|
||||
>
|
||||
{copied === "pid" ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">安全校验码 (Key)</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0a1628] border-gray-700 text-white font-mono text-sm"
|
||||
value={localSettings.alipay.securityKey || ""}
|
||||
onChange={(e) => updateAlipay("securityKey", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700/50 pt-4 space-y-4">
|
||||
<h4 className="text-white font-medium flex items-center gap-2">
|
||||
<ExternalLink className="w-4 h-4 text-[#38bdac]" />
|
||||
跳转链接配置
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">支付宝收款码/跳转链接</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
|
||||
placeholder="https://qr.alipay.com/... 或收款码图片URL"
|
||||
value={localSettings.alipay.qrCode || ""}
|
||||
onChange={(e) => updateAlipay("qrCode", e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">用户点击支付宝支付后显示的二维码</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* USDT配置 */}
|
||||
<TabsContent value="usdt" className="space-y-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-[#26A17B] flex items-center gap-2">
|
||||
<Bitcoin className="w-5 h-5" />
|
||||
USDT配置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置加密货币收款地址</CardDescription>
|
||||
</div>
|
||||
<Switch checked={localSettings.usdt.enabled} onCheckedChange={(c) => updateUsdt("enabled", c)} />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">网络类型</Label>
|
||||
<select
|
||||
className="w-full bg-[#0a1628] border border-gray-700 text-white rounded-md p-2"
|
||||
value={localSettings.usdt.network}
|
||||
onChange={(e) => updateUsdt("network", e.target.value)}
|
||||
>
|
||||
<option value="TRC20">TRC20 (波场)</option>
|
||||
<option value="ERC20">ERC20 (以太坊)</option>
|
||||
<option value="BEP20">BEP20 (币安链)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">收款地址</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white font-mono text-sm"
|
||||
placeholder="T... (TRC20地址)"
|
||||
value={localSettings.usdt.address || ""}
|
||||
onChange={(e) => updateUsdt("address", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">汇率 (1 USD = ? CNY)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={localSettings.usdt.exchangeRate}
|
||||
onChange={(e) => updateUsdt("exchangeRate", Number.parseFloat(e.target.value) || 7.2)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* PayPal配置 */}
|
||||
<TabsContent value="paypal" className="space-y-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-[#169BD7] flex items-center gap-2">
|
||||
<Globe className="w-5 h-5" />
|
||||
PayPal配置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置PayPal收款账户</CardDescription>
|
||||
</div>
|
||||
<Switch checked={localSettings.paypal.enabled} onCheckedChange={(c) => updatePaypal("enabled", c)} />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">PayPal邮箱</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="your@email.com"
|
||||
value={localSettings.paypal.email || ""}
|
||||
onChange={(e) => updatePaypal("email", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">汇率 (1 USD = ? CNY)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={localSettings.paypal.exchangeRate}
|
||||
onChange={(e) => updatePaypal("exchangeRate", Number.parseFloat(e.target.value) || 7.2)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/admin/qrcodes/loading.tsx
Normal file
3
app/admin/qrcodes/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
225
app/admin/qrcodes/page.tsx
Normal file
225
app/admin/qrcodes/page.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { QrCode, Upload, Link, ExternalLink, Copy, Check, HelpCircle } from "lucide-react"
|
||||
|
||||
export default function QRCodesPage() {
|
||||
const { settings, updateSettings } = useStore()
|
||||
const [liveQRUrls, setLiveQRUrls] = useState("")
|
||||
const [wechatGroupUrl, setWechatGroupUrl] = useState("")
|
||||
const [copied, setCopied] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setLiveQRUrls(settings.liveQRCodes?.[0]?.urls?.join("\n") || "")
|
||||
setWechatGroupUrl(settings.paymentMethods?.wechat?.groupQrCode || "")
|
||||
}, [settings])
|
||||
|
||||
const handleCopy = (text: string, field: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopied(field)
|
||||
setTimeout(() => setCopied(""), 2000)
|
||||
}
|
||||
|
||||
const handleSaveLiveQR = () => {
|
||||
const urls = liveQRUrls
|
||||
.split("\n")
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
const updatedLiveQRCodes = [...(settings.liveQRCodes || [])]
|
||||
if (updatedLiveQRCodes[0]) {
|
||||
updatedLiveQRCodes[0].urls = urls
|
||||
} else {
|
||||
updatedLiveQRCodes.push({ id: "live-1", name: "微信群活码", urls, clickCount: 0 })
|
||||
}
|
||||
updateSettings({ liveQRCodes: updatedLiveQRCodes })
|
||||
alert("群活码配置已保存!")
|
||||
}
|
||||
|
||||
const handleSaveWechatGroup = () => {
|
||||
updateSettings({
|
||||
paymentMethods: {
|
||||
...settings.paymentMethods,
|
||||
wechat: {
|
||||
...settings.paymentMethods.wechat,
|
||||
groupQrCode: wechatGroupUrl,
|
||||
},
|
||||
},
|
||||
})
|
||||
alert("微信群链接已保存!用户支付成功后将自动跳转")
|
||||
}
|
||||
|
||||
// 测试跳转
|
||||
const handleTestJump = () => {
|
||||
if (wechatGroupUrl) {
|
||||
window.open(wechatGroupUrl, "_blank")
|
||||
} else {
|
||||
alert("请先配置微信群链接")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-5xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-white">微信群活码管理</h2>
|
||||
<p className="text-gray-400 mt-1">配置微信群跳转链接,用户支付后自动跳转加群</p>
|
||||
</div>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<div className="mb-6 bg-[#07C160]/10 border border-[#07C160]/30 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<HelpCircle className="w-5 h-5 text-[#07C160] flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium mb-2 text-[#07C160]">微信群活码配置指南</p>
|
||||
<div className="text-[#07C160]/80 space-y-2">
|
||||
<p className="font-medium">方法一:使用草料活码(推荐)</p>
|
||||
<ol className="list-decimal list-inside space-y-1 pl-2">
|
||||
<li>
|
||||
访问{" "}
|
||||
<a href="https://cli.im/url" target="_blank" className="underline" rel="noreferrer">
|
||||
草料二维码
|
||||
</a>{" "}
|
||||
创建活码
|
||||
</li>
|
||||
<li>上传微信群二维码图片,生成永久链接</li>
|
||||
<li>复制生成的短链接填入下方配置</li>
|
||||
<li>群满后可直接在草料后台更换新群码,链接不变</li>
|
||||
</ol>
|
||||
<p className="font-medium mt-3">方法二:直接使用微信群链接</p>
|
||||
<ol className="list-decimal list-inside space-y-1 pl-2">
|
||||
<li>微信打开目标群 → 右上角"..." → 群二维码</li>
|
||||
<li>长按二维码 → 识别二维码 → 复制链接</li>
|
||||
<li>或使用第三方工具解析二维码获取链接</li>
|
||||
</ol>
|
||||
<p className="text-[#07C160]/60 mt-2">注意:微信原生群二维码7天后失效,建议使用草料活码</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* 主要配置 - 支付后跳转 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[#07C160] flex items-center gap-2">
|
||||
<QrCode className="w-5 h-5" />
|
||||
支付成功跳转链接(核心配置)
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">用户支付完成后自动跳转到此链接,进入指定微信群</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="wechat-group-url" className="text-gray-300 flex items-center gap-2">
|
||||
<Link className="w-4 h-4" />
|
||||
微信群链接 / 活码链接
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="wechat-group-url"
|
||||
placeholder="https://cli.im/xxxxx 或 https://weixin.qq.com/g/..."
|
||||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 flex-1"
|
||||
value={wechatGroupUrl}
|
||||
onChange={(e) => setWechatGroupUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="border-gray-700 bg-transparent hover:bg-gray-700/50"
|
||||
onClick={() => handleCopy(wechatGroupUrl, "group")}
|
||||
>
|
||||
{copied === "group" ? (
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
支持格式:草料短链、微信群链接(https://weixin.qq.com/g/...)、企业微信链接等
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button onClick={handleSaveWechatGroup} className="flex-1 bg-[#07C160] hover:bg-[#06AD51] text-white">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
保存配置
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleTestJump}
|
||||
variant="outline"
|
||||
className="border-[#07C160] text-[#07C160] hover:bg-[#07C160]/10 bg-transparent"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
测试跳转
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 多群轮换配置 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<QrCode className="w-5 h-5 text-[#38bdac]" />
|
||||
多群轮换(高级配置)
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置多个群链接,系统自动轮换分配,避免单群满员</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="group-urls" className="text-gray-300 flex items-center gap-2">
|
||||
<Link className="w-4 h-4" />
|
||||
多个群链接(每行一个)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="group-urls"
|
||||
placeholder={`https://cli.im/group1\nhttps://cli.im/group2\nhttps://cli.im/group3`}
|
||||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 min-h-[120px] font-mono text-sm"
|
||||
value={liveQRUrls}
|
||||
onChange={(e) => setLiveQRUrls(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">每行填写一个群链接,系统将按顺序或随机分配</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-[#0a1628] rounded-lg border border-gray-700/50">
|
||||
<span className="text-sm text-gray-400">已配置群数量</span>
|
||||
<span className="font-bold text-[#38bdac]">{liveQRUrls.split("\n").filter(Boolean).length} 个</span>
|
||||
</div>
|
||||
<Button onClick={handleSaveLiveQR} className="w-full bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
保存多群配置
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 常见问题 */}
|
||||
<div className="mt-6 bg-[#0f2137] rounded-xl p-4 border border-gray-700/50">
|
||||
<h4 className="text-white font-medium mb-3">常见问题</h4>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="text-[#38bdac]">Q: 为什么推荐使用草料活码?</p>
|
||||
<p className="text-gray-400">
|
||||
A: 草料活码是永久链接,群满后可直接在后台更换新群码,无需修改网站配置。微信原生群码7天失效。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[#38bdac]">Q: 支付后没有跳转怎么办?</p>
|
||||
<p className="text-gray-400">
|
||||
A: 1) 检查链接是否正确填写 2) 部分浏览器可能拦截弹窗,用户需手动允许 3) 建议使用https开头的链接
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[#38bdac]">Q: 如何获取企业微信群链接?</p>
|
||||
<p className="text-gray-400">A: 企业微信后台 → 客户联系 → 加入群聊 → 获取永久有效的群二维码链接</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/admin/settings/loading.tsx
Normal file
3
app/admin/settings/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
224
app/admin/settings/page.tsx
Normal file
224
app/admin/settings/page.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { Save, Settings, Users, DollarSign } from "lucide-react"
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { settings, updateSettings } = useStore()
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
sectionPrice: settings.sectionPrice,
|
||||
baseBookPrice: settings.baseBookPrice,
|
||||
distributorShare: settings.distributorShare,
|
||||
authorInfo: settings.authorInfo,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSettings({
|
||||
sectionPrice: settings.sectionPrice,
|
||||
baseBookPrice: settings.baseBookPrice,
|
||||
distributorShare: settings.distributorShare,
|
||||
authorInfo: settings.authorInfo,
|
||||
})
|
||||
}, [settings])
|
||||
|
||||
const handleSave = () => {
|
||||
updateSettings(localSettings)
|
||||
alert("设置已保存!")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">系统设置</h2>
|
||||
<p className="text-gray-400 mt-1">配置全站基础参数与开关</p>
|
||||
</div>
|
||||
<Button onClick={handleSave} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 基础信息 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Settings className="w-5 h-5 text-[#38bdac]" />
|
||||
基础信息
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">网站显示的基本信息配置</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="author-name" className="text-gray-300">
|
||||
主理人名称
|
||||
</Label>
|
||||
<Input
|
||||
id="author-name"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={localSettings.authorInfo.name}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
authorInfo: { ...prev.authorInfo, name: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="live-time" className="text-gray-300">
|
||||
直播时间
|
||||
</Label>
|
||||
<Input
|
||||
id="live-time"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={localSettings.authorInfo.liveTime}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
authorInfo: { ...prev.authorInfo, liveTime: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description" className="text-gray-300">
|
||||
简介描述
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={localSettings.authorInfo.description}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
authorInfo: { ...prev.authorInfo, description: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 价格设置 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5 text-[#38bdac]" />
|
||||
价格设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置书籍和章节的定价</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">单节价格 (元)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={localSettings.sectionPrice}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
sectionPrice: Number.parseFloat(e.target.value) || 1,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">整本价格 (元)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={localSettings.baseBookPrice}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
baseBookPrice: Number.parseFloat(e.target.value) || 9.9,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 分销设置 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-[#38bdac]" />
|
||||
分销设置
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置分销比例和奖励规则</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label className="text-gray-300">分销者分成比例</Label>
|
||||
<span className="text-2xl font-bold text-[#38bdac]">{localSettings.distributorShare}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[localSettings.distributorShare]}
|
||||
onValueChange={([value]) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
distributorShare: value,
|
||||
}))
|
||||
}
|
||||
max={100}
|
||||
step={5}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-gray-400">
|
||||
<span>作者获得: {100 - localSettings.distributorShare}%</span>
|
||||
<span>分销者获得: {localSettings.distributorShare}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 功能开关 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">功能开关</CardTitle>
|
||||
<CardDescription className="text-gray-400">控制系统核心模块的启用状态</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="maintenance-mode" className="flex flex-col space-y-1">
|
||||
<span className="text-white">维护模式</span>
|
||||
<span className="font-normal text-xs text-gray-500">启用后前台将显示维护中页面</span>
|
||||
</Label>
|
||||
<Switch id="maintenance-mode" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="payment-enabled" className="flex flex-col space-y-1">
|
||||
<span className="text-white">全站支付</span>
|
||||
<span className="font-normal text-xs text-gray-500">关闭后所有支付功能将暂停</span>
|
||||
</Label>
|
||||
<Switch id="payment-enabled" defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="referral-enabled" className="flex flex-col space-y-1">
|
||||
<span className="text-white">分销系统</span>
|
||||
<span className="font-normal text-xs text-gray-500">是否允许用户生成邀请链接</span>
|
||||
</Label>
|
||||
<Switch id="referral-enabled" defaultChecked />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
app/admin/users/loading.tsx
Normal file
3
app/admin/users/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
136
app/admin/users/page.tsx
Normal file
136
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, Suspense } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useStore, type User } from "@/lib/store"
|
||||
import { Search, UserPlus, Eye, Trash2 } from "lucide-react"
|
||||
|
||||
function UsersContent() {
|
||||
const { getAllUsers, deleteUser } = useStore()
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setUsers(getAllUsers())
|
||||
}, [getAllUsers])
|
||||
|
||||
const filteredUsers = users.filter((u) => u.nickname.includes(searchTerm) || u.phone.includes(searchTerm))
|
||||
|
||||
const handleDelete = (userId: string) => {
|
||||
if (confirm("确定要删除这个用户吗?")) {
|
||||
deleteUser(userId)
|
||||
setUsers(getAllUsers())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">用户管理</h2>
|
||||
<p className="text-gray-400 mt-1">共 {users.length} 位注册用户</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索用户..."
|
||||
className="pl-10 bg-[#0f2137] border-gray-700 text-white placeholder:text-gray-500 w-64"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] hover:bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">用户信息</TableHead>
|
||||
<TableHead className="text-gray-400">手机号</TableHead>
|
||||
<TableHead className="text-gray-400">购买状态</TableHead>
|
||||
<TableHead className="text-gray-400">分销收益</TableHead>
|
||||
<TableHead className="text-gray-400">注册时间</TableHead>
|
||||
<TableHead className="text-right text-gray-400">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<TableRow key={user.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
|
||||
{user.nickname.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-white">{user.nickname}</p>
|
||||
<p className="text-xs text-gray-500">ID: {user.id.slice(0, 8)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-300">{user.phone}</TableCell>
|
||||
<TableCell>
|
||||
{user.hasFullBook ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">全书已购</Badge>
|
||||
) : user.purchasedSections.length > 0 ? (
|
||||
<Badge className="bg-blue-500/20 text-blue-400 hover:bg-blue-500/20 border-0">
|
||||
已购 {user.purchasedSections.length} 节
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-gray-500 border-gray-600">
|
||||
未购买
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-white font-medium">¥{user.earnings?.toFixed(2) || "0.00"}</TableCell>
|
||||
<TableCell className="text-gray-400">{new Date(user.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" className="text-gray-400 hover:text-white hover:bg-gray-700/50">
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={() => handleDelete(user.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredUsers.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-12 text-gray-500">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<UsersContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
3
app/admin/withdrawals/loading.tsx
Normal file
3
app/admin/withdrawals/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
172
app/admin/withdrawals/page.tsx
Normal file
172
app/admin/withdrawals/page.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Check, Clock, Wallet, History } from "lucide-react"
|
||||
|
||||
export default function WithdrawalsPage() {
|
||||
const { withdrawals, completeWithdrawal } = useStore()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
const pendingWithdrawals = withdrawals?.filter((w) => w.status === "pending") || []
|
||||
const historyWithdrawals =
|
||||
withdrawals
|
||||
?.filter((w) => w.status !== "pending")
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) || []
|
||||
|
||||
const totalPending = pendingWithdrawals.reduce((sum, w) => sum + w.amount, 0)
|
||||
|
||||
const handleApprove = (id: string) => {
|
||||
if (confirm("确认打款并完成此提现申请吗?")) {
|
||||
completeWithdrawal(id)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-6xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-white">提现管理</h1>
|
||||
<p className="text-gray-400 mt-1">
|
||||
待处理 {pendingWithdrawals.length} 笔,共 ¥{totalPending.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
{/* 待处理申请 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<div className="p-2 rounded-lg bg-orange-500/20">
|
||||
<Clock className="w-5 h-5 text-orange-400" />
|
||||
</div>
|
||||
待处理申请 ({pendingWithdrawals.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{pendingWithdrawals.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Wallet className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500">暂无待处理申请</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-[#0a1628] text-gray-400">
|
||||
<th className="p-4 text-left font-medium">申请时间</th>
|
||||
<th className="p-4 text-left font-medium">用户</th>
|
||||
<th className="p-4 text-left font-medium">收款方式</th>
|
||||
<th className="p-4 text-left font-medium">收款账号</th>
|
||||
<th className="p-4 text-left font-medium">金额</th>
|
||||
<th className="p-4 text-right font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
{pendingWithdrawals.map((w) => (
|
||||
<tr key={w.id} className="hover:bg-[#0a1628] transition-colors">
|
||||
<td className="p-4 text-gray-400">{new Date(w.createdAt).toLocaleString()}</td>
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="font-medium text-white">{w.name}</p>
|
||||
<p className="text-xs text-gray-500 font-mono">{w.userId.slice(0, 8)}...</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Badge
|
||||
className={
|
||||
w.method === "wechat"
|
||||
? "bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0"
|
||||
: "bg-blue-500/20 text-blue-400 hover:bg-blue-500/20 border-0"
|
||||
}
|
||||
>
|
||||
{w.method === "wechat" ? "微信" : "支付宝"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4 font-mono text-gray-300">{w.account}</td>
|
||||
<td className="p-4">
|
||||
<span className="font-bold text-orange-400">¥{w.amount.toFixed(2)}</span>
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleApprove(w.id)}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
确认打款
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 处理历史 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<div className="p-2 rounded-lg bg-gray-700/50">
|
||||
<History className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
处理历史
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{historyWithdrawals.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<History className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500">暂无历史记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-[#0a1628] text-gray-400">
|
||||
<th className="p-4 text-left font-medium">申请时间</th>
|
||||
<th className="p-4 text-left font-medium">处理时间</th>
|
||||
<th className="p-4 text-left font-medium">用户</th>
|
||||
<th className="p-4 text-left font-medium">渠道</th>
|
||||
<th className="p-4 text-left font-medium">金额</th>
|
||||
<th className="p-4 text-left font-medium">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
{historyWithdrawals.map((w) => (
|
||||
<tr key={w.id} className="hover:bg-[#0a1628] transition-colors">
|
||||
<td className="p-4 text-gray-400">{new Date(w.createdAt).toLocaleString()}</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{w.completedAt ? new Date(w.completedAt).toLocaleString() : "-"}
|
||||
</td>
|
||||
<td className="p-4 font-medium text-white">{w.name}</td>
|
||||
<td className="p-4 text-gray-300">{w.method === "wechat" ? "微信" : "支付宝"}</td>
|
||||
<td className="p-4 font-medium text-white">¥{w.amount.toFixed(2)}</td>
|
||||
<td className="p-4">
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||
已完成
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user