Update soul-content project

This commit is contained in:
卡若
2025-12-29 14:01:37 +08:00
commit 087849d509
1112 changed files with 401606 additions and 0 deletions

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

152
app/about/page.tsx Normal file
View 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>
69,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

Binary file not shown.

View 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
View 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
View File

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

82
app/admin/login/page.tsx Normal file
View 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
View 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
View 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 -&gt; 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>
)
}

View 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>
)
}

View 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
View 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>
)
}

View 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

Binary file not shown.

55
app/api/config/route.ts Normal file
View 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
View 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 })
}
}

View 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
View 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 })
}
}

View 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 });
}

View 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 });
}

View 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
View 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>
)
}

View 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

View 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
View 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>
)
}

View File

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

151
app/my/page.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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} />
}

View 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();