Update soul-content project
This commit is contained in:
BIN
app/.DS_Store
vendored
Normal file
BIN
app/.DS_Store
vendored
Normal file
Binary file not shown.
152
app/about/page.tsx
Normal file
152
app/about/page.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { ChevronLeft, Clock, MessageCircle, BookOpen, Users, Award, TrendingUp } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useState } from "react"
|
||||
import { QRCodeModal } from "@/components/modules/marketing/qr-code-modal"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
export default function AboutPage() {
|
||||
const [showQRModal, setShowQRModal] = useState(false)
|
||||
const { settings } = useStore()
|
||||
const authorInfo = settings?.authorInfo || {
|
||||
name: "卡若",
|
||||
description: "连续创业者,私域运营专家",
|
||||
liveTime: "06:00-09:00",
|
||||
platform: "Soul派对房",
|
||||
}
|
||||
|
||||
const milestones = [
|
||||
{ year: "2012", event: "开始做游戏推广,从魔兽世界外挂代理起步" },
|
||||
{ year: "2015", event: "转型电商,做天猫虚拟充值,月流水380万" },
|
||||
{ year: "2017", event: "团队扩张到200人,年流水3000万" },
|
||||
{ year: "2018", event: "公司破产,负债数百万,开始全国旅行反思" },
|
||||
{ year: "2019", event: "重新出发,专注私域运营和个人IP" },
|
||||
{ year: "2024", event: "在Soul派对房每日直播,分享真实商业故事" },
|
||||
]
|
||||
|
||||
const stats = [
|
||||
{ icon: BookOpen, value: "55+", label: "真实案例" },
|
||||
{ icon: Users, value: "10000+", label: "派对房听众" },
|
||||
{ icon: Award, value: "15年", label: "创业经验" },
|
||||
{ icon: TrendingUp, value: "3000万", label: "最高年流水" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a1628] text-white pb-20">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-[#0a1628]/90 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center">
|
||||
<Link href="/" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span>返回</span>
|
||||
</Link>
|
||||
<h1 className="flex-1 text-center text-lg font-semibold">关于作者</h1>
|
||||
<div className="w-16" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Author Card */}
|
||||
<div className="bg-gradient-to-br from-[#38bdac]/20 to-[#0f2137] rounded-2xl p-8 border border-[#38bdac]/30 mb-8">
|
||||
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-[#38bdac] to-[#1a5a50] flex items-center justify-center text-4xl font-bold text-white">
|
||||
{authorInfo.name.charAt(0)}
|
||||
</div>
|
||||
<div className="text-center md:text-left flex-1">
|
||||
<h2 className="text-2xl font-bold text-white mb-2">{authorInfo.name}</h2>
|
||||
<p className="text-gray-400 mb-4">{authorInfo.description}</p>
|
||||
<div className="flex items-center justify-center md:justify-start gap-4 text-sm">
|
||||
<span className="flex items-center gap-1 text-[#38bdac]">
|
||||
<Clock className="w-4 h-4" />
|
||||
每日 {authorInfo.liveTime}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-gray-400">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
{authorInfo.platform}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setShowQRModal(true)} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
加入派对群
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-4 border border-gray-700/50 text-center"
|
||||
>
|
||||
<stat.icon className="w-6 h-6 text-[#38bdac] mx-auto mb-2" />
|
||||
<p className="text-2xl font-bold text-white">{stat.value}</p>
|
||||
<p className="text-gray-400 text-sm">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Introduction */}
|
||||
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-6 border border-gray-700/50 mb-8">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">关于这本书</h3>
|
||||
<div className="space-y-4 text-gray-300 leading-relaxed">
|
||||
<p>"这不是一本教你成功的鸡汤书。"</p>
|
||||
<p>
|
||||
这是我每天早上6点到9点,在Soul派对房和几百个陌生人分享的真实故事。 有人来找项目,有人来找钱,有人来找方向。
|
||||
</p>
|
||||
<p>
|
||||
我见过凌晨四点还在撸运费险的年轻人,见过七十岁还在开滴滴做生意的老人,
|
||||
见过一个月赚七八块却拼命倒卖游戏金币的大学生。
|
||||
</p>
|
||||
<p className="text-[#38bdac] font-semibold">"社会不是靠努力,是靠洞察与选择。"</p>
|
||||
<p>
|
||||
这本书,就是把那些在派对房里讲过的、能让人清醒的故事,整理成文字。每个案例都真实发生过,每个教训都是用钱换来的。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-6 border border-gray-700/50 mb-8">
|
||||
<h3 className="text-lg font-semibold text-white mb-6">创业历程</h3>
|
||||
<div className="space-y-4">
|
||||
{milestones.map((item, index) => (
|
||||
<div key={index} className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-3 h-3 rounded-full bg-[#38bdac]" />
|
||||
{index < milestones.length - 1 && <div className="w-0.5 h-full bg-gray-700 mt-1" />}
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<p className="text-[#38bdac] font-semibold">{item.year}</p>
|
||||
<p className="text-gray-300">{item.event}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="bg-gradient-to-r from-[#38bdac]/10 to-[#1a3a4a]/50 rounded-2xl p-6 border border-[#38bdac]/30 text-center">
|
||||
<h3 className="text-xl font-semibold text-white mb-2">想听更多真实故事?</h3>
|
||||
<p className="text-gray-400 mb-6">每天早上6-9点,卡若在Soul派对房免费分享</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button onClick={() => setShowQRModal(true)} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
加入派对群
|
||||
</Button>
|
||||
<Link href="/chapters">
|
||||
<Button variant="outline" className="border-gray-600 text-white hover:bg-gray-700/50 bg-transparent">
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
开始阅读
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<QRCodeModal isOpen={showQRModal} onClose={() => setShowQRModal(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
app/admin/.DS_Store
vendored
Normal file
BIN
app/admin/.DS_Store
vendored
Normal file
Binary file not shown.
82
app/admin/content/page.tsx
Normal file
82
app/admin/content/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client"
|
||||
|
||||
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"
|
||||
|
||||
export default function ContentPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight">内容管理</h2>
|
||||
<Button>发布新内容</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="chapters" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="chapters">章节管理</TabsTrigger>
|
||||
<TabsTrigger value="articles">文章管理</TabsTrigger>
|
||||
<TabsTrigger value="hooks">钩子配置</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="chapters" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>章节列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border p-4 text-center text-muted-foreground">
|
||||
暂无章节数据,请连接数据库同步
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="articles" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>文章列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border p-4 text-center text-muted-foreground">
|
||||
暂无文章数据
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="hooks" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>引流钩子配置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor="hook-chapter">触发章节</Label>
|
||||
<Select defaultValue="3">
|
||||
<SelectTrigger id="hook-chapter">
|
||||
<SelectValue placeholder="选择章节" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">第一章</SelectItem>
|
||||
<SelectItem value="2">第二章</SelectItem>
|
||||
<SelectItem value="3">第三章 (默认)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label htmlFor="message">引流文案</Label>
|
||||
<Textarea placeholder="输入引导用户加群的文案..." id="message" defaultValue="阅读更多精彩内容,请加入Soul创业实验派对群..." />
|
||||
</div>
|
||||
<Button>保存配置</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 Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { LayoutDashboard, FileText, Users, CreditCard, QrCode, Settings, LogOut, ChevronLeft } 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()
|
||||
|
||||
// Simple admin check (in real app, use robust auth)
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
// router.push("/my") // Commented out for easier development access
|
||||
}
|
||||
}, [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: QrCode, label: "二维码", href: "/admin/qrcodes" },
|
||||
{ icon: Settings, label: "系统设置", href: "/admin/settings" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0f172a]">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-[#1e293b] text-white flex flex-col border-r border-gray-800">
|
||||
<div className="p-6 border-b border-gray-800">
|
||||
<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-2">
|
||||
{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]/10 text-[#38bdac] border border-[#38bdac]/20"
|
||||
: "text-gray-400 hover:bg-gray-800 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-800">
|
||||
<Link
|
||||
href="/my"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-400 hover:text-white rounded-lg hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">返回前台</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-auto bg-[#0f172a]">
|
||||
{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
|
||||
}
|
||||
82
app/admin/login/page.tsx
Normal file
82
app/admin/login/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Lock, User } 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 handleLogin = () => {
|
||||
setError("")
|
||||
const success = adminLogin(username, password)
|
||||
if (success) {
|
||||
router.push("/admin")
|
||||
} else {
|
||||
setError("用户名或密码错误")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a1628] text-white flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">管理后台</h1>
|
||||
<p className="text-gray-500">一场SOUL的创业实验场</p>
|
||||
</div>
|
||||
|
||||
{/* Login form */}
|
||||
<div className="bg-[#0f2137] rounded-2xl p-8 border border-gray-700/50">
|
||||
<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"
|
||||
/>
|
||||
</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"
|
||||
onKeyDown={(e) => e.key === "Enter" && handleLogin()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
||||
|
||||
<Button onClick={handleLogin} className="w-full bg-[#38bdac] hover:bg-[#2da396] text-white py-5">
|
||||
登录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500 text-xs text-center mt-6">默认账号: admin / key123456</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
96
app/admin/page.tsx
Normal file
96
app/admin/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"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-500" },
|
||||
{ title: "总收入", value: `¥${totalRevenue.toFixed(2)}`, icon: TrendingUp, color: "text-green-500" },
|
||||
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-500" },
|
||||
{ title: "转化率", value: `${totalUsers > 0 ? ((totalPurchases / totalUsers) * 100).toFixed(1) : 0}%`, icon: BookOpen, color: "text-orange-500" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto text-white">
|
||||
<h1 className="text-2xl font-bold mb-8">数据概览</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-[#1e293b] border-gray-700">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-400">{stat.title}</CardTitle>
|
||||
<stat.icon className={`w-4 h-4 ${stat.color}`} />
|
||||
</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-[#1e293b] border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">最近订单</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{purchases.slice(-5).reverse().map((p) => (
|
||||
<div key={p.id} className="flex items-center justify-between p-4 bg-[#0f172a] rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{p.sectionTitle || "整本购买"}</p>
|
||||
<p className="text-xs text-gray-400">{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-500">{p.paymentMethod}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{purchases.length === 0 && (
|
||||
<p className="text-gray-500 text-center py-4">暂无订单数据</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#1e293b] border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">新注册用户</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{users.slice(-5).reverse().map((u) => (
|
||||
<div key={u.id} className="flex items-center justify-between p-4 bg-[#0f172a] rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center text-xs">
|
||||
{u.nickname.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{u.nickname}</p>
|
||||
<p className="text-xs text-gray-400">{u.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{new Date(u.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
<p className="text-gray-500 text-center py-4">暂无用户数据</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
248
app/admin/payment/page.tsx
Normal file
248
app/admin/payment/page.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
"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 } from "lucide-react"
|
||||
|
||||
export default function PaymentConfigPage() {
|
||||
const { settings, updateSettings, fetchSettings } = useStore()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [localSettings, setLocalSettings] = useState(settings.paymentMethods)
|
||||
|
||||
// Sync with store on mount
|
||||
useEffect(() => {
|
||||
setLocalSettings(settings.paymentMethods)
|
||||
}, [settings.paymentMethods])
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true)
|
||||
// Update store (and local storage)
|
||||
updateSettings({ paymentMethods: localSettings })
|
||||
|
||||
// Simulate API call delay
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
setLoading(false)
|
||||
alert("配置已保存!")
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setLoading(true)
|
||||
await fetchSettings()
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
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 }
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-5xl mx-auto text-white">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-2">支付配置</h1>
|
||||
<p className="text-gray-400">配置微信、支付宝及USDT支付参数</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" onClick={handleRefresh} className="border-gray-600 text-gray-300 hover:text-white 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="grid grid-cols-1 gap-8">
|
||||
{/* Wechat Pay */}
|
||||
<Card className="bg-[#1e293b] border-gray-700">
|
||||
<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">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.406-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>
|
||||
微信支付
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置微信商户号及API密钥</CardDescription>
|
||||
</div>
|
||||
<Switch
|
||||
checked={localSettings.wechat.enabled}
|
||||
onCheckedChange={(c) => updateWechat('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">网站应用 AppID</Label>
|
||||
<Input
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="wx..."
|
||||
value={localSettings.wechat.websiteAppId || ''}
|
||||
onChange={(e) => updateWechat('websiteAppId', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">网站应用 AppSecret</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="......"
|
||||
value={localSettings.wechat.websiteAppSecret || ''}
|
||||
onChange={(e) => updateWechat('websiteAppSecret', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">商户号 (MchId)</Label>
|
||||
<Input
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="160..."
|
||||
value={localSettings.wechat.merchantId || ''}
|
||||
onChange={(e) => updateWechat('merchantId', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">API v3 密钥</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="......"
|
||||
value={localSettings.wechat.apiKey || ''}
|
||||
onChange={(e) => updateWechat('apiKey', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">个人收款码 URL (兜底方案)</Label>
|
||||
<Input
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="/images/wechat-pay.png"
|
||||
value={localSettings.wechat.qrCode || ''}
|
||||
onChange={(e) => updateWechat('qrCode', e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">当未配置企业支付参数时,将显示此二维码进行个人收款</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Alipay */}
|
||||
<Card className="bg-[#1e293b] border-gray-700">
|
||||
<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">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M8.77 20.62l9.92-4.33c-.12-.33-.24-.66-.38-.99-.14-.33-.3-.66-.47-.99H8.08c-2.2 0-3.99-1.79-3.99-3.99V8.08c0-2.2 1.79-3.99 3.99-3.99h7.84c2.2 0 3.99 1.79 3.99 3.99v2.24h-8.66c-.55 0-1 .45-1 1s.45 1 1 1h10.66c-.18 1.73-.71 3.36-1.53 4.83l-2.76 1.2c-.74-1.69-1.74-3.24-2.93-4.6-.52-.59-1.11-1.13-1.76-1.59H4.09v4.24c0 2.2 1.79 3.99 3.99 3.99h.69v.23z"/></svg>
|
||||
支付宝
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">配置支付宝PID及密钥</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>
|
||||
<Input
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="2088..."
|
||||
value={localSettings.alipay.partnerId || ''}
|
||||
onChange={(e) => updateAlipay('partnerId', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">安全校验码 (Key)</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="......"
|
||||
value={localSettings.alipay.securityKey || ''}
|
||||
onChange={(e) => updateAlipay('securityKey', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">个人收款码 URL (兜底方案)</Label>
|
||||
<Input
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
placeholder="/images/alipay.png"
|
||||
value={localSettings.alipay.qrCode || ''}
|
||||
onChange={(e) => updateAlipay('qrCode', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* USDT */}
|
||||
<Card className="bg-[#1e293b] border-gray-700">
|
||||
<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">
|
||||
<span className="font-bold">₮</span> 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="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">网络协议</Label>
|
||||
<Input
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
value={localSettings.usdt.network || 'TRC20'}
|
||||
onChange={(e) => updateUsdt('network', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">汇率 (USD -> CNY)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0f172a] border-gray-600 text-white"
|
||||
value={localSettings.usdt.exchangeRate || 7.2}
|
||||
onChange={(e) => updateUsdt('exchangeRate', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">钱包地址</Label>
|
||||
<Input
|
||||
className="bg-[#0f172a] border-gray-600 text-white font-mono"
|
||||
placeholder="T..."
|
||||
value={localSettings.usdt.address || ''}
|
||||
onChange={(e) => updateUsdt('address', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
58
app/admin/qrcodes/page.tsx
Normal file
58
app/admin/qrcodes/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
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 { Separator } from "@/components/ui/separator"
|
||||
|
||||
export default function QRCodesPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">二维码管理</h2>
|
||||
<p className="text-muted-foreground">管理用于引流和支付的二维码图片。</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>私域引流码</CardTitle>
|
||||
<CardDescription>用于文章底部、个人中心等位置的加群/加好友二维码。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-6 h-48 bg-slate-50">
|
||||
<span className="text-muted-foreground">预览区域</span>
|
||||
</div>
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor="traffic-qr">上传新图片</Label>
|
||||
<Input id="traffic-qr" type="file" />
|
||||
</div>
|
||||
<Button className="w-full">更新引流码</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>群活码</CardTitle>
|
||||
<CardDescription>用于派对活动引流的动态群活码。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-6 h-48 bg-slate-50">
|
||||
<span className="text-muted-foreground">预览区域</span>
|
||||
</div>
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor="group-qr">上传新图片</Label>
|
||||
<Input id="group-qr" type="file" />
|
||||
</div>
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor="group-url">活码链接 (可选)</Label>
|
||||
<Input id="group-url" placeholder="https://..." defaultValue="https://soul.cn/party" />
|
||||
</div>
|
||||
<Button className="w-full">更新群活码</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
66
app/admin/settings/page.tsx
Normal file
66
app/admin/settings/page.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
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"
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">系统设置</h2>
|
||||
<p className="text-muted-foreground">配置全站基础参数与开关。</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>基础信息</CardTitle>
|
||||
<CardDescription>网站显示的基本信息配置。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="site-name">网站名称</Label>
|
||||
<Input id="site-name" defaultValue="一场Soul的创业实验" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="author">主理人</Label>
|
||||
<Input id="author" defaultValue="卡若" />
|
||||
</div>
|
||||
<Button>保存基础信息</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>功能开关</CardTitle>
|
||||
<CardDescription>控制系统核心模块的启用状态。</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<Label htmlFor="maintenance-mode" className="flex flex-col space-y-1">
|
||||
<span>维护模式</span>
|
||||
<span className="font-normal text-xs text-muted-foreground">启用后前台将显示维护中页面</span>
|
||||
</Label>
|
||||
<Switch id="maintenance-mode" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<Label htmlFor="payment-enabled" className="flex flex-col space-y-1">
|
||||
<span>全站支付</span>
|
||||
<span className="font-normal text-xs text-muted-foreground">关闭后所有支付功能将暂停</span>
|
||||
</Label>
|
||||
<Switch id="payment-enabled" defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<Label htmlFor="referral-enabled" className="flex flex-col space-y-1">
|
||||
<span>分销系统</span>
|
||||
<span className="font-normal text-xs text-muted-foreground">是否允许用户生成邀请链接</span>
|
||||
</Label>
|
||||
<Switch id="referral-enabled" defaultChecked />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
59
app/admin/users/page.tsx
Normal file
59
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } 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"
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight">用户管理</h2>
|
||||
<div className="flex w-full max-w-sm items-center space-x-2">
|
||||
<Input type="text" placeholder="搜索用户..." />
|
||||
<Button type="submit">搜索</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">ID</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>绑定状态</TableHead>
|
||||
<TableHead>注册时间</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">1001</TableCell>
|
||||
<TableCell>测试用户A</TableCell>
|
||||
<TableCell className="text-green-600">已绑定</TableCell>
|
||||
<TableCell>2025-12-28</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm">查看</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">1002</TableCell>
|
||||
<TableCell>游客_8392</TableCell>
|
||||
<TableCell className="text-yellow-600">未绑定</TableCell>
|
||||
<TableCell>2025-12-29</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm">查看</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
135
app/admin/withdrawals/page.tsx
Normal file
135
app/admin/withdrawals/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useStore, type Withdrawal } from "@/lib/store"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Check, X, Clock } 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 handleApprove = (id: string) => {
|
||||
if (confirm("确认打款并完成此提现申请吗?")) {
|
||||
completeWithdrawal(id)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-6">提现管理</h1>
|
||||
|
||||
<div className="grid gap-6">
|
||||
{/* Pending Requests */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-orange-500" />
|
||||
待处理申请 ({pendingWithdrawals.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{pendingWithdrawals.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">暂无待处理申请</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-gray-50 text-gray-700">
|
||||
<tr>
|
||||
<th className="p-3">申请时间</th>
|
||||
<th className="p-3">用户ID</th>
|
||||
<th className="p-3">姓名</th>
|
||||
<th className="p-3">渠道</th>
|
||||
<th className="p-3">账号</th>
|
||||
<th className="p-3">金额</th>
|
||||
<th className="p-3 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{pendingWithdrawals.map((w) => (
|
||||
<tr key={w.id} className="hover:bg-gray-50">
|
||||
<td className="p-3">{new Date(w.createdAt).toLocaleString()}</td>
|
||||
<td className="p-3 font-mono text-xs">{w.userId.slice(0, 8)}...</td>
|
||||
<td className="p-3 font-medium">{w.name}</td>
|
||||
<td className="p-3">
|
||||
<span className={`px-2 py-1 rounded-full text-xs text-white ${w.method === "wechat" ? "bg-green-600" : "bg-blue-600"}`}>
|
||||
{w.method === "wechat" ? "微信" : "支付宝"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-3 font-mono">{w.account}</td>
|
||||
<td className="p-3 font-bold text-orange-600">¥{w.amount.toFixed(2)}</td>
|
||||
<td className="p-3 text-right">
|
||||
<Button size="sm" onClick={() => handleApprove(w.id)} className="bg-green-600 hover:bg-green-700 text-white">
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
确认打款
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* History */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>处理历史</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{historyWithdrawals.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">暂无历史记录</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-gray-50 text-gray-700">
|
||||
<tr>
|
||||
<th className="p-3">申请时间</th>
|
||||
<th className="p-3">处理时间</th>
|
||||
<th className="p-3">姓名</th>
|
||||
<th className="p-3">渠道</th>
|
||||
<th className="p-3">金额</th>
|
||||
<th className="p-3">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{historyWithdrawals.map((w) => (
|
||||
<tr key={w.id} className="hover:bg-gray-50">
|
||||
<td className="p-3 text-gray-500">{new Date(w.createdAt).toLocaleString()}</td>
|
||||
<td className="p-3 text-gray-500">{w.completedAt ? new Date(w.completedAt).toLocaleString() : "-"}</td>
|
||||
<td className="p-3">{w.name}</td>
|
||||
<td className="p-3">
|
||||
{w.method === "wechat" ? "微信" : "支付宝"}
|
||||
</td>
|
||||
<td className="p-3 font-medium">¥{w.amount.toFixed(2)}</td>
|
||||
<td className="p-3">
|
||||
<span className="px-2 py-1 rounded-full text-xs text-green-600 border border-green-200 bg-green-50">
|
||||
已完成
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
app/api/.DS_Store
vendored
Normal file
BIN
app/api/.DS_Store
vendored
Normal file
Binary file not shown.
55
app/api/config/route.ts
Normal file
55
app/api/config/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
const config = {
|
||||
paymentMethods: {
|
||||
wechat: {
|
||||
enabled: true,
|
||||
qrCode: "/images/wechat-pay.png",
|
||||
account: "卡若",
|
||||
appId: process.env.TENCENT_APP_ID || "1251077262", // From .env or default
|
||||
// 敏感信息后端处理,不完全暴露给前端
|
||||
},
|
||||
alipay: {
|
||||
enabled: true,
|
||||
qrCode: "/images/alipay.png",
|
||||
account: "卡若",
|
||||
appId: process.env.ALIPAY_ACCESS_KEY_ID || "LTAI5t9zkiWmFtHG8qmtdysW", // Using Access Key as placeholder ID
|
||||
},
|
||||
usdt: {
|
||||
enabled: true,
|
||||
network: "TRC20",
|
||||
address: process.env.USDT_WALLET_ADDRESS || "TWeq9xxxxxxxxxxxxxxxxxxxx",
|
||||
exchangeRate: 7.2
|
||||
},
|
||||
paypal: {
|
||||
enabled: false,
|
||||
email: process.env.PAYPAL_CLIENT_ID || "",
|
||||
exchangeRate: 7.2
|
||||
}
|
||||
},
|
||||
marketing: {
|
||||
partyGroup: {
|
||||
url: "https://soul.cn/party",
|
||||
liveCodeUrl: "https://soul.cn/party-live",
|
||||
qrCode: "/images/party-group-qr.png"
|
||||
},
|
||||
banner: {
|
||||
text: "每日早上6-9点,Soul派对房不见不散",
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
authorInfo: {
|
||||
name: "卡若",
|
||||
description: "私域运营与技术公司主理人",
|
||||
liveTime: "06:00-09:00",
|
||||
platform: "Soul"
|
||||
},
|
||||
system: {
|
||||
version: "1.0.0",
|
||||
maintenance: false
|
||||
}
|
||||
};
|
||||
|
||||
return NextResponse.json(config);
|
||||
}
|
||||
28
app/api/content/route.ts
Normal file
28
app/api/content/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const filePath = searchParams.get("path")
|
||||
const sectionId = searchParams.get("sectionId")
|
||||
|
||||
if (!filePath && !sectionId) {
|
||||
return NextResponse.json({ error: "Path or sectionId is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (filePath?.startsWith("custom/")) {
|
||||
// Custom sections have their content stored in localStorage on the client
|
||||
// Return empty content, client will handle it
|
||||
return NextResponse.json({ content: "", isCustom: true })
|
||||
}
|
||||
|
||||
try {
|
||||
const fullPath = path.join(process.cwd(), filePath || "")
|
||||
const content = fs.readFileSync(fullPath, "utf-8")
|
||||
return NextResponse.json({ content, isCustom: false })
|
||||
} catch (error) {
|
||||
console.error("Error reading file:", error)
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 })
|
||||
}
|
||||
}
|
||||
55
app/api/documentation/generate/route.ts
Normal file
55
app/api/documentation/generate/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextResponse, type NextRequest } from "next/server"
|
||||
import { getDocumentationCatalog } from "@/lib/documentation/catalog"
|
||||
import { captureScreenshots } from "@/lib/documentation/screenshot"
|
||||
import { renderDocumentationDocx } from "@/lib/documentation/docx"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
function getBaseUrl(request: NextRequest) {
|
||||
const proto = request.headers.get("x-forwarded-proto") || "http"
|
||||
const host = request.headers.get("x-forwarded-host") || request.headers.get("host")
|
||||
if (!host) return null
|
||||
return `${proto}://${host}`
|
||||
}
|
||||
|
||||
function isAuthorized(request: NextRequest) {
|
||||
const token = process.env.DOCUMENTATION_TOKEN
|
||||
// If no token is configured, allow access (internal tool)
|
||||
if (!token || token === "") return true
|
||||
const header = request.headers.get("x-documentation-token")
|
||||
const query = request.nextUrl.searchParams.get("token")
|
||||
return header === token || query === token
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!isAuthorized(request)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const baseUrl = getBaseUrl(request)
|
||||
if (!baseUrl) {
|
||||
return NextResponse.json({ error: "Host is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const pages = getDocumentationCatalog()
|
||||
const screenshots = await captureScreenshots(pages, {
|
||||
baseUrl,
|
||||
timeoutMs: 60000,
|
||||
viewport: { width: 430, height: 932 },
|
||||
})
|
||||
const docxBuffer = await renderDocumentationDocx(screenshots)
|
||||
|
||||
return new NextResponse(docxBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"Content-Disposition": `attachment; filename="app-documentation.docx"`,
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
12
app/api/menu/route.ts
Normal file
12
app/api/menu/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getBookStructure } from "@/lib/book-file-system"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const structure = getBookStructure()
|
||||
return NextResponse.json(structure)
|
||||
} catch (error) {
|
||||
console.error("Error generating menu:", error)
|
||||
return NextResponse.json({ error: "Failed to generate menu" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
9
app/api/payment/checkout/route.js
Normal file
9
app/api/payment/checkout/route.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { paymentService } from '../../../services/paymentService';
|
||||
import { connectDB } from '../../../lib/db';
|
||||
|
||||
export async function POST(req) {
|
||||
await connectDB();
|
||||
const { orderId, gateway } = await req.json();
|
||||
const result = await paymentService.pay(orderId, gateway);
|
||||
return Response.json({ success: true, result });
|
||||
}
|
||||
9
app/api/payment/create/route.js
Normal file
9
app/api/payment/create/route.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { paymentService } from '../../../services/paymentService';
|
||||
import { connectDB } from '../../../lib/db';
|
||||
|
||||
export async function POST(req) {
|
||||
await connectDB();
|
||||
const data = await req.json();
|
||||
const order = await paymentService.createOrder(data);
|
||||
return Response.json({ success: true, order });
|
||||
}
|
||||
20
app/api/payment/notify/[gateway]/route.js
Normal file
20
app/api/payment/notify/[gateway]/route.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Order } from '../../../../models/Order';
|
||||
import { connectDB } from '../../../../lib/db';
|
||||
|
||||
export async function POST(req, { params }) {
|
||||
await connectDB();
|
||||
const { gateway } = await params;
|
||||
const data = await req.json();
|
||||
// 根据gateway处理通知并更新订单状态
|
||||
// 这是一个简化版本,实际需要验证签名和处理不同gateway的逻辑
|
||||
const orderId = data.out_trade_no || data.order_id || data.metadata?.order_id;
|
||||
|
||||
if (orderId) {
|
||||
const order = await Order.findById(orderId);
|
||||
if (order) {
|
||||
order.status = 'paid';
|
||||
await order.save();
|
||||
}
|
||||
}
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
54
app/chapters/page.tsx
Normal file
54
app/chapters/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import Link from "next/link"
|
||||
import { ChevronLeft } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { specialSections, FULL_BOOK_PRICE } from "@/lib/book-data"
|
||||
import { getBookStructure } from "@/lib/book-file-system"
|
||||
import { ChaptersList } from "@/components/chapters-list"
|
||||
|
||||
import { BuyFullBookButton } from "@/components/buy-full-book-button"
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function ChaptersPage() {
|
||||
const parts = getBookStructure()
|
||||
|
||||
// Format special sections for the client component
|
||||
const specialSectionsData = {
|
||||
preface: { title: specialSections.preface.title },
|
||||
epilogue: { title: specialSections.epilogue.title }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a1628] text-white">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-[#0a1628]/90 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span>返回</span>
|
||||
</Link>
|
||||
<h1 className="text-lg font-semibold">目录</h1>
|
||||
<BuyFullBookButton size="sm" price={9.9} className="bg-[#38bdac] hover:bg-[#2da396] text-white" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<main className="max-w-4xl mx-auto px-4 py-8">
|
||||
<ChaptersList parts={parts} specialSections={specialSectionsData} />
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<div className="mt-12 bg-gradient-to-r from-[#38bdac]/20 to-[#0f2137] rounded-2xl p-6 border border-[#38bdac]/30">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-white text-lg font-semibold mb-1">购买整本书,省82%</h3>
|
||||
<p className="text-gray-400">全部55节内容,永久阅读,后续更新免费获取</p>
|
||||
</div>
|
||||
<BuyFullBookButton size="lg" price={9.9} className="bg-[#38bdac] hover:bg-[#2da396] text-white px-8">
|
||||
立即购买 ¥9.9
|
||||
</BuyFullBookButton>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
app/components/PaymentTest.js
Normal file
1
app/components/PaymentTest.js
Normal file
@@ -0,0 +1 @@
|
||||
import React from 'react';\nimport { Button } from '@/components/ui/button';\nimport { paymentService } from '../services/paymentService';\n\nexport function PaymentTest() {\n const handleTestPayment = async () => {\n try {\n const order = { amount: 9.9, currency: 'CNY' };\n const result = await paymentService.pay(order);\n console.log('Payment result:', result);\n } catch (error) {\n console.error('Payment error:', error);\n }\n };\n\n return (\n <div>\n <Button onClick={handleTestPayment}>测试支付</Button>\n </div>\n );\n}\n
|
||||
1
app/components/__tests__/PaymentTest.test.js
Normal file
1
app/components/__tests__/PaymentTest.test.js
Normal file
@@ -0,0 +1 @@
|
||||
import { render, fireEvent } from '@testing-library/react';\nimport { PaymentTest } from '../PaymentTest';\n\ndescribe('PaymentTest Component', () => {\n it('renders without crashing', () => {\n const { getByText } = render(<PaymentTest />);\n expect(getByText('测试支付')).toBeInTheDocument();\n });\n\n it('calls handleTestPayment on button click', () => {\n const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});\n const { getByText } = render(<PaymentTest />);\n fireEvent.click(getByText('测试支付'));\n expect(consoleSpy).toHaveBeenCalledWith('Payment result:', expect.anything());\n consoleSpy.mockRestore();\n });\n});\n
|
||||
112
app/docs/page.tsx
Normal file
112
app/docs/page.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, CreditCard, Share2, FileText, Code } from "lucide-react"
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a1628] text-white pb-20">
|
||||
<div className="sticky top-0 z-10 bg-[#0a1628]/95 backdrop-blur-md border-b border-gray-700/50">
|
||||
<div className="max-w-2xl mx-auto flex items-center gap-4 p-4">
|
||||
<Link href="/" className="p-2 -ml-2">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<h1 className="text-lg font-semibold">开发者文档</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
{/* Payment Configuration */}
|
||||
<section className="bg-[#0f2137]/60 rounded-xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<CreditCard className="w-6 h-6 text-[#38bdac]" />
|
||||
<h2 className="text-xl font-semibold">支付配置</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-gray-300 text-sm">
|
||||
<div>
|
||||
<h3 className="text-white font-medium mb-2">微信支付配置</h3>
|
||||
<p className="mb-2">1. 登录微信开放平台获取网站AppID和AppSecret</p>
|
||||
<p className="mb-2">2. 登录微信公众平台获取服务号AppID和AppSecret</p>
|
||||
<p className="mb-2">3. 登录微信商户平台获取商户号和API密钥</p>
|
||||
<div className="bg-[#0a1628] rounded-lg p-3 mt-2">
|
||||
<code className="text-xs text-gray-400">
|
||||
{`网站AppID: wx432c93e275548671
|
||||
服务号AppID: wx7c0dbf34ddba300d
|
||||
商户号: 1318592501`}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-white font-medium mb-2">支付宝配置</h3>
|
||||
<p className="mb-2">1. 登录支付宝开放平台获取合作者身份PID</p>
|
||||
<p className="mb-2">2. 获取安全校验码Key</p>
|
||||
<p className="mb-2">3. 开通手机网站支付功能</p>
|
||||
<div className="bg-[#0a1628] rounded-lg p-3 mt-2">
|
||||
<code className="text-xs text-gray-400">
|
||||
{`合作者身份(PID): 2088511801157159
|
||||
安全校验码(Key): lz6ey1h3kl9...`}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Distribution System */}
|
||||
<section className="bg-[#0f2137]/60 rounded-xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Share2 className="w-6 h-6 text-[#38bdac]" />
|
||||
<h2 className="text-xl font-semibold">分销机制</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-gray-300 text-sm">
|
||||
<p>分销比例可在后台自由设置(0-100%)</p>
|
||||
<div className="bg-[#0a1628] rounded-lg p-4">
|
||||
<p className="text-white mb-2">收益计算公式:</p>
|
||||
<code className="text-[#38bdac]">分销收益 = 订单金额 × 分销比例%</code>
|
||||
</div>
|
||||
<p>例: 用户A通过B的邀请码购买¥9.9整本书,分销比例90%</p>
|
||||
<p>则B获得 9.9 × 90% = ¥8.91 收益</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Withdrawal */}
|
||||
<section className="bg-[#0f2137]/60 rounded-xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<FileText className="w-6 h-6 text-[#38bdac]" />
|
||||
<h2 className="text-xl font-semibold">提现说明</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-gray-300 text-sm">
|
||||
<p>1. 最低提现金额: ¥10</p>
|
||||
<p>2. 提现周期: T+1到账</p>
|
||||
<p>3. 支持提现方式: 微信、支付宝、银行卡</p>
|
||||
<p>4. 提现手续费: 0%</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* API Reference */}
|
||||
<section className="bg-[#0f2137]/60 rounded-xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Code className="w-6 h-6 text-[#38bdac]" />
|
||||
<h2 className="text-xl font-semibold">API接口</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 text-gray-300 text-sm">
|
||||
<div className="bg-[#0a1628] rounded-lg p-4">
|
||||
<p className="text-gray-400 mb-2">获取章节内容</p>
|
||||
<code className="text-[#38bdac]">GET /api/content?id=1.1</code>
|
||||
</div>
|
||||
<div className="bg-[#0a1628] rounded-lg p-4">
|
||||
<p className="text-gray-400 mb-2">飞书文档同步</p>
|
||||
<code className="text-[#38bdac]">POST /api/feishu/sync</code>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
3
app/documentation/capture/loading.tsx
Normal file
3
app/documentation/capture/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
78
app/documentation/capture/page.tsx
Normal file
78
app/documentation/capture/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
|
||||
export default function DocumentationCapturePage() {
|
||||
const searchParams = useSearchParams()
|
||||
const path = searchParams.get("path") || "/"
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [timeoutReached, setTimeoutReached] = useState(false)
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
|
||||
const src = useMemo(() => {
|
||||
if (!path.startsWith("/")) return `/${path}`
|
||||
return path
|
||||
}, [path])
|
||||
|
||||
useEffect(() => {
|
||||
setLoaded(false)
|
||||
setTimeoutReached(false)
|
||||
setLoadError(null)
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
if (!loaded) {
|
||||
setTimeoutReached(true)
|
||||
}
|
||||
}, 60000)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [src, loaded])
|
||||
|
||||
const handleLoad = () => {
|
||||
setLoaded(true)
|
||||
setTimeoutReached(false)
|
||||
}
|
||||
|
||||
const handleError = () => {
|
||||
setLoadError("页面加载失败")
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="w-[430px] h-[932px] border border-gray-200 bg-white relative overflow-hidden">
|
||||
<iframe
|
||||
data-doc-iframe="true"
|
||||
data-loaded={loaded ? "true" : "false"}
|
||||
src={src}
|
||||
className="w-full h-full border-0"
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
title={`Capture: ${path}`}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||
/>
|
||||
|
||||
{!loaded && !timeoutReached && !loadError && (
|
||||
<div className="absolute inset-0 bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-500">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(timeoutReached || loadError) && (
|
||||
<div className="fixed left-0 top-0 right-0 bg-red-600 text-white text-sm px-3 py-2 text-center">
|
||||
{loadError || "页面加载超时"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loaded && (
|
||||
<div className="fixed left-0 bottom-0 right-0 bg-green-600 text-white text-xs px-3 py-1 text-center">
|
||||
页面已加载: {path}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
306
app/documentation/page.tsx
Normal file
306
app/documentation/page.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { getDocumentationCatalog, type DocumentationPage } from "@/lib/documentation/catalog"
|
||||
import { FileText, Download, Loader2, CheckCircle, XCircle, Eye, RefreshCw } from "lucide-react"
|
||||
|
||||
type PageStatus = "pending" | "loading" | "success" | "error"
|
||||
|
||||
type PageState = {
|
||||
page: DocumentationPage
|
||||
status: PageStatus
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default function DocumentationToolPage() {
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState<string | null>(null)
|
||||
const [pageStates, setPageStates] = useState<PageState[]>([])
|
||||
const [previewPath, setPreviewPath] = useState<string | null>(null)
|
||||
const [showPreview, setShowPreview] = useState(false)
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
|
||||
const pages = useMemo(() => getDocumentationCatalog(), [])
|
||||
|
||||
const groupedPages = useMemo(() => {
|
||||
const groups: Record<string, DocumentationPage[]> = {}
|
||||
for (const page of pages) {
|
||||
if (!groups[page.group]) groups[page.group] = []
|
||||
groups[page.group].push(page)
|
||||
}
|
||||
return groups
|
||||
}, [pages])
|
||||
|
||||
useEffect(() => {
|
||||
setPageStates(pages.map((page) => ({ page, status: "pending" })))
|
||||
}, [pages])
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setError(null)
|
||||
setIsGenerating(true)
|
||||
setProgress(0)
|
||||
setCurrentPage(null)
|
||||
setPageStates(pages.map((page) => ({ page, status: "loading" })))
|
||||
|
||||
try {
|
||||
// Simulate progress while waiting for the API
|
||||
let progressValue = 0
|
||||
const progressInterval = setInterval(() => {
|
||||
progressValue += 2
|
||||
const pageIndex = Math.floor((progressValue / 100) * pages.length)
|
||||
const nextPage = pages[Math.min(pageIndex, pages.length - 1)]
|
||||
if (nextPage) setCurrentPage(nextPage.title)
|
||||
setProgress(Math.min(progressValue, 90))
|
||||
|
||||
// Update page states to show progress
|
||||
setPageStates((prev) =>
|
||||
prev.map((s, idx) => ({
|
||||
...s,
|
||||
status: idx < pageIndex ? "success" : idx === pageIndex ? "loading" : "pending",
|
||||
})),
|
||||
)
|
||||
}, 800)
|
||||
|
||||
// Get token from URL if provided
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const token = urlParams.get("token") || ""
|
||||
|
||||
const response = await fetch(`/api/documentation/generate${token ? `?token=${token}` : ""}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { "x-documentation-token": token } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
clearInterval(progressInterval)
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "")
|
||||
let errorMessage = `生成失败(${response.status})`
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
errorMessage = json.error || errorMessage
|
||||
} catch {
|
||||
if (text) errorMessage = text
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
setProgress(100)
|
||||
setCurrentPage("完成")
|
||||
setPageStates(pages.map((page) => ({ page, status: "success" })))
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `应用功能文档_${new Date().toISOString().slice(0, 10)}.docx`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e)
|
||||
setError(message)
|
||||
setPageStates((prev) => prev.map((s) => ({ ...s, status: "error" })))
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreview = useCallback((path: string) => {
|
||||
setPreviewPath(path)
|
||||
setShowPreview(true)
|
||||
}, [])
|
||||
|
||||
const getStatusIcon = (status: PageStatus) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return <div className="w-4 h-4 rounded-full bg-gray-600" />
|
||||
case "loading":
|
||||
return <Loader2 className="w-4 h-4 animate-spin text-teal-400" />
|
||||
case "success":
|
||||
return <CheckCircle className="w-4 h-4 text-green-500" />
|
||||
case "error":
|
||||
return <XCircle className="w-4 h-4 text-red-500" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background text-foreground p-4 pb-24">
|
||||
<div className="max-w-md mx-auto space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<FileText className="w-6 h-6 text-teal-400" />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">文档生成器</h1>
|
||||
<p className="text-xs text-muted-foreground">自动截图并导出专业文档</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="bg-card border border-border rounded-xl p-4 space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">页面总数</span>
|
||||
<span className="font-medium text-teal-400">{pages.length} 个</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">分组数量</span>
|
||||
<span className="font-medium">{Object.keys(groupedPages).length} 组</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">输出格式</span>
|
||||
<span className="font-medium">Word (.docx)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
{isGenerating && (
|
||||
<div className="bg-card border border-border rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">生成进度</span>
|
||||
<span className="font-medium text-teal-400">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-teal-500 to-cyan-400 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
{currentPage && <p className="text-xs text-muted-foreground truncate">正在处理: {currentPage}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="bg-destructive/10 border border-destructive/30 text-destructive rounded-xl p-3 text-sm">
|
||||
<p className="font-medium mb-1">生成失败</p>
|
||||
<p className="text-xs opacity-80">{error}</p>
|
||||
<p className="text-xs mt-2 opacity-60">提示: 如需授权,请在URL中添加 ?token=your_token</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating}
|
||||
className="w-full bg-gradient-to-r from-teal-500 to-cyan-500 text-white rounded-xl py-3.5 font-medium disabled:opacity-60 flex items-center justify-center gap-2 shadow-lg shadow-teal-500/20"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>正在生成文档...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-5 h-5" />
|
||||
<span>一键生成 Word 文档</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Page List */}
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<span>文档目录预览</span>
|
||||
<span className="text-xs opacity-60">({pages.length}个页面)</span>
|
||||
</h2>
|
||||
|
||||
{Object.entries(groupedPages).map(([group, groupPages]) => (
|
||||
<div key={group} className="bg-card border border-border rounded-xl overflow-hidden">
|
||||
<div className="px-3 py-2 bg-muted/50 border-b border-border">
|
||||
<h3 className="text-sm font-medium text-teal-400">{group}</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-border">
|
||||
{groupPages.map((page, index) => {
|
||||
const state = pageStates.find((s) => s.page.path === page.path)
|
||||
return (
|
||||
<div
|
||||
key={page.path}
|
||||
className="px-3 py-2.5 flex items-center gap-3 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground w-5">{index + 1}</span>
|
||||
{state && getStatusIcon(state.status)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{page.title}</p>
|
||||
{page.subtitle && <p className="text-xs text-muted-foreground truncate">{page.subtitle}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handlePreview(page.path)}
|
||||
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
|
||||
title="预览页面"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="bg-card border border-border rounded-xl p-4 space-y-3">
|
||||
<h3 className="text-sm font-medium">文档包含内容</h3>
|
||||
<ul className="text-xs text-muted-foreground space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span>自动生成的目录结构</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span>所有页面的真实截图(iPhone 14 Pro Max尺寸)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span>每个页面的功能说明与路径</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span>按功能模块分组整理</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<p className="text-xs text-muted-foreground text-center">生成过程需要30-60秒,请耐心等待</p>
|
||||
</div>
|
||||
|
||||
{/* Preview Modal */}
|
||||
{showPreview && previewPath && (
|
||||
<div className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-card rounded-2xl w-full max-w-md overflow-hidden border border-border">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 className="font-medium text-sm">页面预览</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => iframeRef.current?.contentWindow?.location.reload()}
|
||||
className="p-1.5 text-muted-foreground hover:text-foreground rounded-lg"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPreview(false)}
|
||||
className="p-1.5 text-muted-foreground hover:text-foreground rounded-lg"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full aspect-[430/932] bg-white">
|
||||
<iframe ref={iframeRef} src={previewPath} className="w-full h-full" title="Page Preview" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
143
app/globals.css
Normal file
143
app/globals.css
Normal file
@@ -0,0 +1,143 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
|
||||
/* Custom app tokens */
|
||||
--app-bg: #0a1628;
|
||||
--app-card: #0f2137;
|
||||
--app-brand: #38bdac;
|
||||
--app-brand-hover: #2da396;
|
||||
--app-text: #ffffff;
|
||||
--app-text-muted: #9ca3af;
|
||||
--app-border: rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: "Geist", "Geist Fallback";
|
||||
--font-mono: "Geist Mono", "Geist Mono Fallback";
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
/* Custom semantic tokens for app */
|
||||
--color-app-bg: var(--app-bg);
|
||||
--color-app-card: var(--app-card);
|
||||
--color-app-brand: var(--app-brand);
|
||||
--color-app-brand-hover: var(--app-brand-hover);
|
||||
--color-app-text: var(--app-text);
|
||||
--color-app-text-muted: var(--app-text-muted);
|
||||
--color-app-border: var(--app-border);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
47
app/layout.tsx
Normal file
47
app/layout.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { Geist, Geist_Mono } from "next/font/google"
|
||||
import { Analytics } from "@vercel/analytics/next"
|
||||
import "./globals.css"
|
||||
import { LayoutWrapper } from "@/components/layout-wrapper"
|
||||
|
||||
const _geist = Geist({ subsets: ["latin"] })
|
||||
const _geistMono = Geist_Mono({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "一场SOUL的创业实验场 - 卡若",
|
||||
description: "来自Soul派对房的真实商业故事,每天早上6-9点免费分享",
|
||||
generator: "v0.app",
|
||||
icons: {
|
||||
icon: [
|
||||
{
|
||||
url: "/icon-light-32x32.png",
|
||||
media: "(prefers-color-scheme: light)",
|
||||
},
|
||||
{
|
||||
url: "/icon-dark-32x32.png",
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
},
|
||||
{
|
||||
url: "/icon.svg",
|
||||
type: "image/svg+xml",
|
||||
},
|
||||
],
|
||||
apple: "/apple-icon.png",
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className={`bg-[#0a1628]`}>
|
||||
<LayoutWrapper>{children}</LayoutWrapper>
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
1
app/lib/db.js
Normal file
1
app/lib/db.js
Normal file
@@ -0,0 +1 @@
|
||||
import mongoose from 'mongoose';\n\nlet isConnected = false;\n\nexport async function connectDB() {\n if (isConnected) return;\n\n try {\n await mongoose.connect(process.env.MONGODB_URI, {\n useNewUrlParser: true,\n useUnifiedTopology: true,\n });\n isConnected = true;\n console.log('MongoDB connected');\n } catch (error) {\n console.error('MongoDB connection error:', error);\n throw error;\n }\n}
|
||||
1
app/models/Order.js
Normal file
1
app/models/Order.js
Normal file
@@ -0,0 +1 @@
|
||||
import mongoose from 'mongoose';\n\nconst orderSchema = new mongoose.Schema({\n userId: { type: String, required: true },\n amount: { type: Number, required: true },\n currency: { type: String, default: 'CNY' },\n status: { type: String, default: 'pending' },\n createdAt: { type: Date, default: Date.now },\n updatedAt: { type: Date, default: Date.now },\n});\n\nexport const Order = mongoose.model('Order', orderSchema);
|
||||
1
app/models/PayTrade.js
Normal file
1
app/models/PayTrade.js
Normal file
@@ -0,0 +1 @@
|
||||
import mongoose from 'mongoose';\n\nconst payTradeSchema = new mongoose.Schema({\n orderId: { type: mongoose.Schema.Types.ObjectId, ref: 'Order', required: true },\n gateway: { type: String, required: true },\n transactionId: String,\n status: { type: String, default: 'initiated' },\n response: Object,\n createdAt: { type: Date, default: Date.now },\n updatedAt: { type: Date, default: Date.now },\n});\n\nexport const PayTrade = mongoose.model('PayTrade', payTradeSchema);
|
||||
BIN
app/my/.DS_Store
vendored
Normal file
BIN
app/my/.DS_Store
vendored
Normal file
Binary file not shown.
151
app/my/page.tsx
Normal file
151
app/my/page.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { User, ShoppingBag, Share2, LogOut, ChevronRight, BookOpen } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { AuthModal } from "@/components/modules/auth/auth-modal"
|
||||
import { getFullBookPrice } from "@/lib/book-data"
|
||||
|
||||
export default function MyPage() {
|
||||
const { user, isLoggedIn, logout } = useStore()
|
||||
const [showAuthModal, setShowAuthModal] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-app-bg flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-app-brand" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<main className="min-h-screen bg-app-bg text-app-text pb-20 flex flex-col items-center justify-center">
|
||||
<div className="p-4 w-full">
|
||||
<div className="max-w-xs mx-auto text-center">
|
||||
<div className="w-14 h-14 mx-auto mb-3 rounded-full bg-app-card flex items-center justify-center">
|
||||
<User className="w-7 h-7 text-app-text-muted" />
|
||||
</div>
|
||||
<h2 className="text-base font-semibold mb-1">登录后查看更多</h2>
|
||||
<p className="text-app-text-muted text-xs mb-4">查看购买记录、分销收益</p>
|
||||
<button
|
||||
onClick={() => setShowAuthModal(true)}
|
||||
className="bg-app-brand hover:bg-app-brand-hover text-white px-6 py-2.5 rounded-full font-medium text-sm"
|
||||
>
|
||||
立即登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<AuthModal isOpen={showAuthModal} onClose={() => setShowAuthModal(false)} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const fullBookPrice = getFullBookPrice()
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-app-bg text-app-text pb-20">
|
||||
{/* User Profile Header */}
|
||||
<div className="bg-gradient-to-b from-app-card to-app-bg p-4 pt-8">
|
||||
<div className="max-w-xs mx-auto">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-11 h-11 rounded-full bg-app-brand/20 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-app-brand" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">{user?.nickname || "用户"}</h2>
|
||||
<p className="text-app-text-muted text-xs">{user?.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="bg-app-card/60 rounded-lg p-2.5 text-center">
|
||||
<p className="text-base font-bold text-app-brand">
|
||||
{user?.hasFullBook ? "全部" : user?.purchasedSections.length || 0}
|
||||
</p>
|
||||
<p className="text-app-text-muted text-xs">已购章节</p>
|
||||
</div>
|
||||
<div className="bg-app-card/60 rounded-lg p-2.5 text-center">
|
||||
<p className="text-base font-bold text-app-brand">¥{(user?.earnings || 0).toFixed(1)}</p>
|
||||
<p className="text-app-text-muted text-xs">累计收益</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="p-4">
|
||||
<div className="max-w-xs mx-auto space-y-2">
|
||||
{/* Purchase prompt */}
|
||||
{!user?.hasFullBook && (
|
||||
<Link href="/chapters" className="block">
|
||||
<div className="bg-gradient-to-r from-app-brand/20 to-app-card rounded-lg p-3 border border-app-brand/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-app-brand" />
|
||||
<span className="text-app-text text-sm">购买整本书</span>
|
||||
</div>
|
||||
<span className="text-app-brand font-bold text-sm">¥{fullBookPrice}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Menu List - simplified, removed settings and docs */}
|
||||
<div className="bg-app-card/60 rounded-lg overflow-hidden">
|
||||
<Link href="/my/purchases" className="flex items-center justify-between p-3 border-b border-app-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingBag className="w-4 h-4 text-app-text-muted" />
|
||||
<span className="text-sm">我的购买</span>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-app-text-muted" />
|
||||
</Link>
|
||||
|
||||
<Link href="/my/referral" className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Share2 className="w-4 h-4 text-app-text-muted" />
|
||||
<span className="text-sm">分销收益</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-app-brand text-xs">¥{(user?.earnings || 0).toFixed(1)}</span>
|
||||
<ChevronRight className="w-4 h-4 text-app-text-muted" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Referral Code */}
|
||||
<div className="bg-app-card/60 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-app-text-muted text-xs">我的邀请码</p>
|
||||
<code className="text-app-brand font-mono text-sm">{user?.referralCode}</code>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(user?.referralCode || "")}
|
||||
className="text-app-text-muted text-xs hover:text-app-text px-2 py-1 rounded bg-app-card"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<button
|
||||
onClick={logout}
|
||||
className="w-full flex items-center justify-center gap-2 p-2.5 text-app-text-muted hover:text-red-400 transition-colors text-sm"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
109
app/my/purchases/page.tsx
Normal file
109
app/my/purchases/page.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { ChevronLeft, BookOpen, CheckCircle } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { bookData, getAllSections } from "@/lib/book-data"
|
||||
|
||||
export default function MyPurchasesPage() {
|
||||
const { user, isLoggedIn } = useStore()
|
||||
|
||||
if (!isLoggedIn || !user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a1628] text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-400 mb-4">请先登录</p>
|
||||
<Link href="/" className="text-[#38bdac] hover:underline">
|
||||
返回首页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const allSections = getAllSections()
|
||||
const purchasedCount = user.hasFullBook ? allSections.length : user.purchasedSections.length
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a1628] text-white">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-[#0a1628]/90 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center">
|
||||
<Link href="/" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span>返回</span>
|
||||
</Link>
|
||||
<h1 className="flex-1 text-center text-lg font-semibold">我的购买</h1>
|
||||
<div className="w-16" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Stats */}
|
||||
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-6 border border-gray-700/50 mb-8">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-white">{purchasedCount}</p>
|
||||
<p className="text-gray-400 text-sm">已购买章节</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-[#38bdac]">
|
||||
{user.hasFullBook ? "全书" : `${purchasedCount}/${allSections.length}`}
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm">{user.hasFullBook ? "已解锁" : "进度"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Purchased sections */}
|
||||
{user.hasFullBook ? (
|
||||
<div className="bg-gradient-to-r from-[#38bdac]/20 to-[#0f2137] rounded-xl p-6 border border-[#38bdac]/30 text-center mb-8">
|
||||
<CheckCircle className="w-12 h-12 text-[#38bdac] mx-auto mb-3" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">您已购买整本书</h3>
|
||||
<p className="text-gray-400">全部55节内容已解锁,可随时阅读</p>
|
||||
</div>
|
||||
) : user.purchasedSections.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<BookOpen className="w-16 h-16 text-gray-600 mx-auto mb-4" />
|
||||
<p className="text-gray-400 mb-4">您还没有购买任何章节</p>
|
||||
<Link href="/chapters" className="text-[#38bdac] hover:underline">
|
||||
去浏览章节
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-gray-400 text-sm mb-4">已购买的章节</h2>
|
||||
{bookData.map((part) => {
|
||||
const purchasedInPart = part.chapters.flatMap((c) =>
|
||||
c.sections.filter((s) => user.purchasedSections.includes(s.id)),
|
||||
)
|
||||
|
||||
if (purchasedInPart.length === 0) return null
|
||||
|
||||
return (
|
||||
<div key={part.id} className="bg-[#0f2137]/40 rounded-xl border border-gray-800/50 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-[#0a1628]/50">
|
||||
<p className="text-gray-400 text-sm">{part.title}</p>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-800/30">
|
||||
{purchasedInPart.map((section) => (
|
||||
<Link
|
||||
key={section.id}
|
||||
href={`/read/${section.id}`}
|
||||
className="flex items-center gap-3 px-4 py-3 hover:bg-[#0f2137]/40 transition-colors"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 text-[#38bdac]" />
|
||||
<span className="text-gray-400 font-mono text-sm">{section.id}</span>
|
||||
<span className="text-gray-300">{section.title}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
233
app/my/referral/page.tsx
Normal file
233
app/my/referral/page.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { ChevronLeft, Copy, Share2, Users, Wallet, MessageCircle, ImageIcon } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useStore, type Purchase } from "@/lib/store"
|
||||
import { PosterModal } from "@/components/modules/referral/poster-modal"
|
||||
import { WithdrawalModal } from "@/components/modules/referral/withdrawal-modal"
|
||||
|
||||
export default function ReferralPage() {
|
||||
const { user, isLoggedIn, settings, getAllPurchases, getAllUsers } = useStore()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showPoster, setShowPoster] = useState(false)
|
||||
const [showWithdrawal, setShowWithdrawal] = useState(false)
|
||||
const [referralPurchases, setReferralPurchases] = useState<Purchase[]>([])
|
||||
const [referralUsers, setReferralUsers] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.referralCode) {
|
||||
const allPurchases = getAllPurchases()
|
||||
const allUsers = getAllUsers()
|
||||
const usersWithMyCode = allUsers.filter((u) => u.referredBy === user.referralCode)
|
||||
const userIds = usersWithMyCode.map((u) => u.id)
|
||||
const myReferralPurchases = allPurchases.filter((p) => userIds.includes(p.userId))
|
||||
setReferralPurchases(myReferralPurchases)
|
||||
setReferralUsers(usersWithMyCode.length)
|
||||
}
|
||||
}, [user, getAllPurchases, getAllUsers])
|
||||
|
||||
if (!isLoggedIn || !user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-app-bg text-app-text flex items-center justify-center pb-20">
|
||||
<div className="text-center">
|
||||
<p className="text-app-text-muted mb-4">请先登录</p>
|
||||
<Link href="/" className="text-app-brand hover:underline">
|
||||
返回首页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const referralLink = `${typeof window !== "undefined" ? window.location.origin : ""}?ref=${user.referralCode}`
|
||||
const distributorShare = settings?.distributorShare || 90
|
||||
const totalEarnings = user.earnings || 0
|
||||
const pendingEarnings = user.pendingEarnings || 0
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(referralLink)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const handleShare = async () => {
|
||||
const shareText = `我正在读《一场SOUL的创业实验场》,每天6-9点的真实商业故事,推荐给你!${referralLink}`
|
||||
try {
|
||||
if (typeof navigator.share === 'function' && typeof navigator.canShare === 'function') {
|
||||
await navigator.share({
|
||||
title: "一场SOUL的创业实验场",
|
||||
text: "来自Soul派对房的真实商业故事",
|
||||
url: referralLink,
|
||||
})
|
||||
} else {
|
||||
await navigator.clipboard.writeText(shareText)
|
||||
alert("分享文案已复制,快去朋友圈或Soul派对分享吧!")
|
||||
}
|
||||
} catch {
|
||||
await navigator.clipboard.writeText(shareText)
|
||||
alert("分享文案已复制!")
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareToWechat = async () => {
|
||||
const shareText = `📖 推荐一本好书《一场SOUL的创业实验场》
|
||||
|
||||
这是卡若每天早上6-9点在Soul派对房分享的真实商业故事,55个真实案例,讲透创业的底层逻辑。
|
||||
|
||||
👉 点击阅读: ${referralLink}
|
||||
|
||||
#创业 #商业思维 #Soul派对`
|
||||
await navigator.clipboard.writeText(shareText)
|
||||
alert("朋友圈文案已复制!\n\n打开微信 → 发朋友圈 → 粘贴即可")
|
||||
}
|
||||
|
||||
const handleShareToSoul = async () => {
|
||||
const shareText = `在Soul派对房听卡若讲了好多真实的创业故事,他把这些故事整理成了一本书《一场SOUL的创业实验场》,推荐给你们~
|
||||
|
||||
每天早上6-9点直播,这本书就是直播内容的精华版。
|
||||
|
||||
链接: ${referralLink}`
|
||||
await navigator.clipboard.writeText(shareText)
|
||||
alert("Soul分享文案已复制!\n\n打开Soul → 发动态 → 粘贴即可")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-app-bg text-app-text pb-24">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-app-bg/90 backdrop-blur-md border-b border-app-border">
|
||||
<div className="max-w-xs mx-auto px-4 py-3 flex items-center">
|
||||
<Link href="/my" className="flex items-center gap-1 text-app-text-muted hover:text-app-text">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<h1 className="flex-1 text-center text-sm font-semibold">分销中心</h1>
|
||||
<div className="w-5" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-xs mx-auto px-4 py-4">
|
||||
{/* Earnings Card */}
|
||||
<div className="bg-gradient-to-br from-app-brand/20 to-app-card rounded-xl p-4 border border-app-brand/30 mb-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="w-4 h-4 text-app-brand" />
|
||||
<span className="text-app-text-muted text-xs">累计收益</span>
|
||||
</div>
|
||||
<span className="text-app-brand text-xs">{distributorShare}%返利</span>
|
||||
</div>
|
||||
|
||||
<p className="text-2xl font-bold text-app-text mb-0.5">¥{totalEarnings.toFixed(2)}</p>
|
||||
<p className="text-app-text-muted text-xs mb-3">待结算: ¥{pendingEarnings.toFixed(2)}</p>
|
||||
|
||||
<Button
|
||||
disabled={totalEarnings < 10}
|
||||
onClick={() => setShowWithdrawal(true)}
|
||||
className="w-full bg-app-brand hover:bg-app-brand-hover text-white h-8 text-xs"
|
||||
>
|
||||
{totalEarnings < 10 ? `满10元可提现` : "申请提现"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
<div className="bg-app-card/60 rounded-lg p-2.5 text-center">
|
||||
<Users className="w-4 h-4 text-app-brand mx-auto mb-1" />
|
||||
<p className="text-base font-bold text-app-text">{referralUsers}</p>
|
||||
<p className="text-app-text-muted text-xs">邀请人数</p>
|
||||
</div>
|
||||
<div className="bg-app-card/60 rounded-lg p-2.5 text-center">
|
||||
<Share2 className="w-4 h-4 text-app-brand mx-auto mb-1" />
|
||||
<p className="text-base font-bold text-app-text">{referralPurchases.length}</p>
|
||||
<p className="text-app-text-muted text-xs">成交订单</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Referral link */}
|
||||
<div className="bg-app-card/60 rounded-xl p-3 border border-app-border mb-3">
|
||||
<p className="text-app-text text-xs font-medium mb-2">我的专属链接</p>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<div className="flex-1 bg-app-bg rounded-lg px-2.5 py-1.5 text-app-text-muted text-xs truncate font-mono">
|
||||
{referralLink}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCopy}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-app-border text-app-text hover:bg-app-card bg-transparent text-xs h-7 px-2"
|
||||
>
|
||||
<Copy className="w-3 h-3 mr-1" />
|
||||
{copied ? "已复制" : "复制"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-app-text-muted text-xs">
|
||||
邀请码: <span className="text-app-brand font-mono">{user.referralCode}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Share buttons - improved for WeChat/Soul */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<Button
|
||||
onClick={() => setShowPoster(true)}
|
||||
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-4 text-xs"
|
||||
>
|
||||
<ImageIcon className="w-4 h-4 mr-2" />
|
||||
生成推广海报
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShareToWechat}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white py-4 text-xs"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
分享到朋友圈
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
variant="outline"
|
||||
className="w-full border-app-border text-app-text hover:bg-app-card bg-transparent py-4 text-xs"
|
||||
>
|
||||
更多分享方式
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PosterModal
|
||||
isOpen={showPoster}
|
||||
onClose={() => setShowPoster(false)}
|
||||
referralLink={referralLink}
|
||||
referralCode={user.referralCode}
|
||||
nickname={user.nickname}
|
||||
/>
|
||||
|
||||
<WithdrawalModal
|
||||
isOpen={showWithdrawal}
|
||||
onClose={() => setShowWithdrawal(false)}
|
||||
availableAmount={totalEarnings}
|
||||
/>
|
||||
|
||||
{/* Recent earnings */}
|
||||
{referralPurchases.length > 0 && (
|
||||
<div className="bg-app-card/60 rounded-xl border border-app-border">
|
||||
<div className="p-2.5 border-b border-app-border">
|
||||
<p className="text-app-text text-xs font-medium">收益明细</p>
|
||||
</div>
|
||||
<div className="divide-y divide-app-border max-h-40 overflow-auto">
|
||||
{referralPurchases.slice(0, 5).map((purchase) => (
|
||||
<div key={purchase.id} className="p-2.5 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-app-text text-xs">{purchase.type === "fullbook" ? "整本书" : "单节"}</p>
|
||||
<p className="text-app-text-muted text-xs">
|
||||
{new Date(purchase.createdAt).toLocaleDateString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-app-brand text-sm font-semibold">
|
||||
+¥{(purchase.referrerEarnings || 0).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
96
app/my/settings/page.tsx
Normal file
96
app/my/settings/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, Phone, Bell, Shield, HelpCircle } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, updateUser } = useStore()
|
||||
const [nickname, setNickname] = useState(user?.nickname || "")
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const handleSave = () => {
|
||||
if (user) {
|
||||
updateUser(user.id, { nickname })
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a1628] text-white pb-20">
|
||||
<div className="sticky top-0 z-10 bg-[#0a1628]/95 backdrop-blur-md border-b border-gray-700/50">
|
||||
<div className="max-w-md mx-auto flex items-center gap-4 p-4">
|
||||
<Link href="/my" className="p-2 -ml-2">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<h1 className="text-lg font-semibold">账户设置</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="max-w-md mx-auto space-y-4">
|
||||
{/* Profile Settings */}
|
||||
<div className="bg-[#0f2137]/60 rounded-xl p-4">
|
||||
<h3 className="text-gray-400 text-sm mb-4">个人信息</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-gray-400 text-xs">昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
className="w-full bg-[#0a1628] border border-gray-700 rounded-lg px-4 py-3 text-white mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-gray-400 text-xs">手机号</label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Phone className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-gray-400">{user?.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="w-full mt-4 bg-[#38bdac] hover:bg-[#2da396] text-white py-3 rounded-lg font-medium"
|
||||
>
|
||||
{saved ? "已保存" : "保存修改"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Other Settings */}
|
||||
<div className="bg-[#0f2137]/60 rounded-xl overflow-hidden">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-700/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<Bell className="w-5 h-5 text-gray-400" />
|
||||
<span>消息通知</span>
|
||||
</div>
|
||||
<div className="w-10 h-5 bg-[#38bdac] rounded-full relative">
|
||||
<div className="w-4 h-4 bg-white rounded-full absolute right-0.5 top-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-700/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-5 h-5 text-gray-400" />
|
||||
<span>隐私设置</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href="/docs" className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<HelpCircle className="w-5 h-5 text-gray-400" />
|
||||
<span>帮助文档</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
26
app/page.tsx
Normal file
26
app/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { BookCover } from "@/components/book-cover"
|
||||
import { BookIntro } from "@/components/book-intro"
|
||||
import { TableOfContents } from "@/components/table-of-contents"
|
||||
import { PurchaseSection } from "@/components/purchase-section"
|
||||
import { Footer } from "@/components/footer"
|
||||
import { getBookStructure } from "@/lib/book-file-system"
|
||||
|
||||
// Force dynamic rendering if we want it to update on every request without rebuild
|
||||
// or use revalidation. For now, we can leave it default (static if no dynamic functions used, but fs usage makes it dynamic in dev usually).
|
||||
// Actually, in App Router, using fs directly in a Server Component usually makes it static at build time unless using dynamic functions.
|
||||
// To ensure it updates when files change in dev, it should be fine.
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function HomePage() {
|
||||
const parts = getBookStructure()
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#0a1628] text-white pb-20">
|
||||
<BookCover />
|
||||
<BookIntro />
|
||||
<TableOfContents parts={parts} />
|
||||
<PurchaseSection />
|
||||
<Footer />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
42
app/read/[id]/page.tsx
Normal file
42
app/read/[id]/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { ChapterContent } from "@/components/chapter-content"
|
||||
import { getSectionBySlug, getChapterBySectionSlug, getSectionContent } from "@/lib/book-file-system"
|
||||
import { specialSections } from "@/lib/book-data"
|
||||
|
||||
interface ReadPageProps {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function ReadPage({ params }: ReadPageProps) {
|
||||
const { id } = await params
|
||||
|
||||
// Check special sections first
|
||||
// Note: Special sections might not have file paths in the dynamic system yet unless mapped
|
||||
// For now, we keep the hardcoded specialSections check but maybe we should map them to files too
|
||||
if (id === "preface") {
|
||||
return <ChapterContent section={specialSections.preface} partTitle="序言" chapterTitle="" />
|
||||
}
|
||||
|
||||
if (id === "epilogue") {
|
||||
return <ChapterContent section={specialSections.epilogue} partTitle="尾声" chapterTitle="" />
|
||||
}
|
||||
|
||||
// Find regular section from dynamic file system
|
||||
const section = getSectionBySlug(id)
|
||||
if (!section) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const context = getChapterBySectionSlug(id)
|
||||
if (!context) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Read content from file
|
||||
const content = getSectionContent(section.filePath)
|
||||
const sectionWithContent = { ...section, content }
|
||||
|
||||
return <ChapterContent section={sectionWithContent} partTitle={context.part.title} chapterTitle={context.chapter.title} />
|
||||
}
|
||||
1
app/services/paymentService.js
Normal file
1
app/services/paymentService.js
Normal file
@@ -0,0 +1 @@
|
||||
import { Order, PayTrade } from '../models/Order';\nimport { PayTrade as PayTradeModel } from '../models/PayTrade';\nimport AlipaySDK from 'alipay-sdk';\nimport WeChatPay from 'wechatpay-node-v3';\nimport Stripe from 'stripe';\nimport * as Paypal from '@paypal/checkout-server-sdk';\nimport Web3 from 'web3';\n\nclass AlipayAdapter {\n constructor() {\n this.sdk = new AlipaySDK({\n appId: process.env.ALIPAY_APP_ID,\n privateKey: process.env.ALIPAY_PRIVATE_KEY,\n alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY,\n gateway: 'https://openapi.alipay.com/gateway.do',\n });\n }\n\n async pay(order) {\n const result = await this.sdk.exec('alipay.trade.app.pay', {\n bizContent: {\n subject: `Payment for order ${order._id}`,\n out_trade_no: order._id.toString(),\n total_amount: order.amount.toString(),\n product_code: 'QUICK_MSECURITY_PAY',\n },\n });\n\n const trade = new PayTradeModel({ orderId: order._id, gateway: 'alipay', response: result });\n await trade.save();\n\n return result;\n }\n}\n\nclass WechatAdapter {\n constructor() {\n this.pay = new WeChatPay({\n appid: process.env.WECHAT_APPID,\n mchid: process.env.WECHAT_MCHID,\n private_key: process.env.WECHAT_PRIVATE_KEY,\n serial_no: process.env.WECHAT_SERIAL_NO,\n apiv3_key: process.env.WECHAT_APIV3_KEY,\n });\n }\n\n async pay(order) {\n const params = {\n out_trade_no: order._id.toString(),\n appid: process.env.WECHAT_APPID,\n description: `Payment for order ${order._id}`,\n amount: { total: Math.floor(order.amount * 100) },\n payer: { openid: 'user_openid' } // 需要替换为实际openid\n };\n const result = await this.pay.jsapi(params);\n\n const trade = new PayTradeModel({ orderId: order._id, gateway: 'wechat', response: result });\n await trade.save();\n\n return result;\n }\n}\n\nclass PaypalAdapter {\n constructor() {\n const environment = new Paypal.core.SandboxEnvironment(process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_CLIENT_SECRET);\n this.client = new Paypal.core.PayPalHttpClient(environment);\n }\n\n async pay(order) {\n const request = new Paypal.orders.OrdersCreateRequest();\n request.requestBody({\n intent: 'CAPTURE',\n purchase_units: [{\n amount: {\n currency_code: order.currency,\n value: order.amount.toString(),\n }\n }]\n });\n const response = await this.client.execute(request);\n\n const trade = new PayTradeModel({ orderId: order._id, gateway: 'paypal', response: response.result });\n await trade.save();\n\n return response.result;\n }\n}\n\nclass StripeAdapter {\n constructor() {\n this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY);\n }\n\n async pay(order) {\n const paymentIntent = await this.stripe.paymentIntents.create({\n amount: Math.floor(order.amount * 100),\n currency: order.currency,\n metadata: { order_id: order._id.toString() },\n });\n\n const trade = new PayTradeModel({ orderId: order._id, gateway: 'stripe', response: paymentIntent });\n await trade.save();\n\n return paymentIntent;\n }\n}\n\nclass UsdtAdapter {\n async pay(order) {\n // 实现USDT支付逻辑,使用web3\n // 示例: 生成支付地址或交易\n const web3 = new Web3(process.env.WEB3_PROVIDER || 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY');\n // TODO: 实现USDT ERC20转账或支付请求\n const result = { paymentAddress: 'generated_usdt_address', amount: order.amount };\n\n const trade = new PayTradeModel({ orderId: order._id, gateway: 'usdt', response: result });\n await trade.save();\n\n return result;\n }\n}\n\nclass PaymentService {\n async createOrder(data) {\n const order = new Order(data);\n await order.save();\n return order;\n }\n\n async pay(orderId, gateway) {\n const order = await Order.findById(orderId);\n if (!order) throw new Error('Order not found');\n\n let adapter;\n switch (gateway) {\n case 'alipay':\n adapter = new AlipayAdapter();\n break;\n case 'wechat':\n adapter = new WechatAdapter();\n break;\n case 'paypal':\n adapter = new PaypalAdapter();\n break;\n case 'stripe':\n adapter = new StripeAdapter();\n break;\n case 'usdt':\n adapter = new UsdtAdapter();\n break;\n default:\n throw new Error('Unsupported gateway');\n }\n\n const result = await adapter.pay(order);\n\n order.status = 'paid'; // 根据实际结果更新\n await order.save();\n\n return result;\n }\n}\n\nexport const paymentService = new PaymentService();
|
||||
Reference in New Issue
Block a user