Files
soul/app/my/referral/page.tsx
卡若 b60edb3d47 feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
2026-01-21 15:49:12 +08:00

570 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import {
ChevronLeft, Copy, Share2, Users, Wallet, MessageCircle, ImageIcon,
TrendingUp, Gift, Check, ArrowRight, Bell, Clock, AlertCircle,
CheckCircle, UserPlus, Settings, Zap, ChevronDown, ChevronUp
} from "lucide-react"
import { useStore, type Purchase } from "@/lib/store"
import { PosterModal } from "@/components/modules/referral/poster-modal"
import { WithdrawalModal } from "@/components/modules/referral/withdrawal-modal"
import { AutoWithdrawModal } from "@/components/modules/distribution/auto-withdraw-modal"
import { RealtimeNotification } from "@/components/modules/distribution/realtime-notification"
// 绑定用户类型
interface BindingUser {
id: string
visitorNickname?: string
visitorPhone?: string
bindingTime: string
expireTime: string
status: 'active' | 'converted' | 'expired'
daysRemaining?: number
commission?: number
orderAmount?: number
}
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 [showAutoWithdraw, setShowAutoWithdraw] = useState(false)
const [referralPurchases, setReferralPurchases] = useState<Purchase[]>([])
const [referralUsers, setReferralUsers] = useState<number>(0)
// 绑定用户相关状态
const [activeBindings, setActiveBindings] = useState<BindingUser[]>([])
const [convertedBindings, setConvertedBindings] = useState<BindingUser[]>([])
const [expiredBindings, setExpiredBindings] = useState<BindingUser[]>([])
const [expiringCount, setExpiringCount] = useState(0)
const [showBindingList, setShowBindingList] = useState(true)
const [activeTab, setActiveTab] = useState<'active' | 'converted' | 'expired'>('active')
// 自动提现状态
const [autoWithdrawEnabled, setAutoWithdrawEnabled] = useState(false)
const [autoWithdrawThreshold, setAutoWithdrawThreshold] = useState(100)
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)
// 模拟绑定数据实际从API获取
loadBindingData()
}
}, [user, getAllPurchases, getAllUsers])
// 加载绑定数据
const loadBindingData = async () => {
// 模拟数据 - 实际项目中从 /api/distribution?type=my-bindings&userId=xxx 获取
const now = new Date()
const mockActiveBindings: BindingUser[] = [
{
id: '1',
visitorNickname: '小明',
visitorPhone: '138****1234',
bindingTime: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
daysRemaining: 5,
},
{
id: '2',
visitorNickname: '小红',
visitorPhone: '139****5678',
bindingTime: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
daysRemaining: 20,
},
{
id: '3',
visitorNickname: '阿强',
visitorPhone: '137****9012',
bindingTime: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
daysRemaining: 2,
},
]
const mockConvertedBindings: BindingUser[] = [
{
id: '4',
visitorNickname: '小李',
visitorPhone: '136****3456',
bindingTime: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
status: 'converted',
commission: 8.91,
orderAmount: 9.9,
},
]
const mockExpiredBindings: BindingUser[] = [
{
id: '5',
visitorNickname: '小王',
visitorPhone: '135****7890',
bindingTime: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString(),
expireTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
status: 'expired',
},
]
setActiveBindings(mockActiveBindings)
setConvertedBindings(mockConvertedBindings)
setExpiredBindings(mockExpiredBindings)
setExpiringCount(mockActiveBindings.filter(b => (b.daysRemaining || 0) <= 7).length)
}
if (!isLoggedIn || !user) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center pb-20">
<div className="text-center glass-card p-8">
<p className="text-[var(--app-text-secondary)] mb-4"></p>
<Link href="/" className="btn-ios inline-block">
</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 getBindingStatusStyle = (daysRemaining?: number) => {
if (!daysRemaining) return 'bg-gray-500/20 text-gray-400'
if (daysRemaining <= 3) return 'bg-red-500/20 text-red-400'
if (daysRemaining <= 7) return 'bg-orange-500/20 text-orange-400'
return 'bg-green-500/20 text-green-400'
}
// 获取绑定状态文本
const getBindingStatusText = (binding: BindingUser) => {
if (binding.status === 'converted') return '已付款'
if (binding.status === 'expired') return '已过期'
if (binding.daysRemaining && binding.daysRemaining <= 3) return `${binding.daysRemaining}天后过期`
if (binding.daysRemaining && binding.daysRemaining <= 7) return `${binding.daysRemaining}`
return `${binding.daysRemaining || 0}`
}
const currentBindings = activeTab === 'active' ? activeBindings : activeTab === 'converted' ? convertedBindings : expiredBindings
return (
<div className="min-h-screen bg-black text-white pb-24 page-transition">
{/* 背景光效 */}
<div className="fixed inset-0 -z-10">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[500px] h-[500px] bg-[var(--app-brand)] opacity-[0.05] blur-[150px] rounded-full" />
</div>
{/* Header - iOS风格 */}
<header className="sticky top-0 z-50 glass-nav safe-top">
<div className="max-w-md mx-auto px-4 py-3 flex items-center">
<Link href="/my" className="w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback">
<ChevronLeft className="w-5 h-5 text-[var(--app-text-secondary)]" />
</Link>
<h1 className="flex-1 text-center font-semibold"></h1>
<div className="flex items-center gap-2">
<RealtimeNotification />
<button
onClick={() => setShowAutoWithdraw(true)}
className="w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback"
>
<Settings className="w-4 h-4 text-[var(--app-text-secondary)]" />
</button>
</div>
</div>
</header>
<main className="max-w-md mx-auto px-4 py-6">
{/* 过期提醒横幅 */}
{expiringCount > 0 && (
<div className="mb-4 glass-card p-4 border border-orange-500/30 bg-orange-500/10">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-orange-500/20 flex items-center justify-center flex-shrink-0">
<Bell className="w-5 h-5 text-orange-400" />
</div>
<div className="flex-1">
<p className="text-white font-medium text-sm">
{expiringCount}
</p>
<p className="text-orange-300/80 text-xs mt-0.5">
30
</p>
</div>
</div>
</div>
)}
{/* 收益卡片 - 毛玻璃渐变 */}
<div className="relative glass-card-heavy p-6 mb-6 overflow-hidden">
{/* 背景装饰 */}
<div className="absolute top-0 right-0 w-32 h-32 bg-[var(--app-brand)] opacity-[0.15] blur-[50px] rounded-full" />
<div className="relative">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center">
<Wallet className="w-5 h-5 text-[var(--app-brand)]" />
</div>
<div>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
<p className="text-[var(--app-brand)] text-xs font-medium">{distributorShare}% </p>
</div>
</div>
<div className="text-right">
<p className="text-3xl font-bold text-white glow-text">¥{totalEarnings.toFixed(2)}</p>
<p className="text-[var(--app-text-tertiary)] text-xs">: ¥{pendingEarnings.toFixed(2)}</p>
</div>
</div>
<div className="flex gap-2">
<button
disabled={totalEarnings < 10}
onClick={() => setShowWithdrawal(true)}
className="flex-1 btn-ios glow disabled:opacity-50 disabled:cursor-not-allowed"
>
{totalEarnings < 10 ? `满10元可提现` : "申请提现"}
</button>
{autoWithdrawEnabled && (
<div className="flex items-center gap-1 px-3 py-2 bg-[var(--app-brand-light)] rounded-xl">
<Zap className="w-4 h-4 text-[var(--app-brand)]" />
<span className="text-[var(--app-brand)] text-xs font-medium"></span>
</div>
)}
</div>
</div>
</div>
{/* 数据统计 */}
<div className="grid grid-cols-4 gap-2 mb-6">
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-white">{activeBindings.length}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-white">{convertedBindings.length}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-orange-400">{expiringCount}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
<div className="glass-card p-3 text-center">
<p className="text-xl font-bold text-white">{referralUsers}</p>
<p className="text-[var(--app-text-tertiary)] text-[10px]"></p>
</div>
</div>
{/* 分销规则说明 */}
<div className="glass-card p-4 mb-6 border border-[var(--app-brand)]/20 bg-[var(--app-brand)]/5">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-lg bg-[var(--app-brand-light)] flex items-center justify-center flex-shrink-0">
<AlertCircle className="w-4 h-4 text-[var(--app-brand)]" />
</div>
<div>
<p className="text-white font-medium text-sm mb-1">广</p>
<ul className="text-[var(--app-text-tertiary)] text-xs space-y-1">
<li> <span className="text-[#FFD700]">5%</span></li>
<li> <span className="text-[var(--app-brand)]">{distributorShare}%</span> </li>
<li> <span className="text-[var(--app-brand)]">30</span></li>
</ul>
</div>
</div>
</div>
{/* 绑定用户列表 */}
<div className="glass-card overflow-hidden mb-6">
<button
onClick={() => setShowBindingList(!showBindingList)}
className="w-full px-5 py-4 flex items-center justify-between border-b border-[var(--app-separator)]"
>
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-[var(--app-brand)]" />
<h3 className="text-white font-semibold"></h3>
<span className="text-[var(--app-text-tertiary)] text-sm">
({activeBindings.length + convertedBindings.length + expiredBindings.length})
</span>
</div>
{showBindingList ? (
<ChevronUp className="w-5 h-5 text-[var(--app-text-tertiary)]" />
) : (
<ChevronDown className="w-5 h-5 text-[var(--app-text-tertiary)]" />
)}
</button>
{showBindingList && (
<>
{/* Tab切换 */}
<div className="flex border-b border-[var(--app-separator)]">
{[
{ key: 'active', label: '绑定中', count: activeBindings.length },
{ key: 'converted', label: '已付款', count: convertedBindings.length },
{ key: 'expired', label: '已过期', count: expiredBindings.length },
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key as typeof activeTab)}
className={`flex-1 py-3 text-sm font-medium transition-colors relative ${
activeTab === tab.key
? 'text-[var(--app-brand)]'
: 'text-[var(--app-text-tertiary)]'
}`}
>
{tab.label} ({tab.count})
{activeTab === tab.key && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-[var(--app-brand)] rounded-full" />
)}
</button>
))}
</div>
{/* 用户列表 */}
<div className="max-h-80 overflow-auto scrollbar-hide">
{currentBindings.length === 0 ? (
<div className="py-12 text-center">
<UserPlus className="w-10 h-10 text-[var(--app-text-tertiary)] mx-auto mb-2" />
<p className="text-[var(--app-text-tertiary)] text-sm"></p>
</div>
) : (
currentBindings.map((binding, idx) => (
<div
key={binding.id}
className={`px-5 py-4 flex items-center justify-between ${
idx !== currentBindings.length - 1 ? 'border-b border-[var(--app-separator)]' : ''
}`}
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
binding.status === 'converted'
? 'bg-green-500/20'
: binding.status === 'expired'
? 'bg-gray-500/20'
: 'bg-[var(--app-brand-light)]'
}`}>
{binding.status === 'converted' ? (
<CheckCircle className="w-5 h-5 text-green-400" />
) : binding.status === 'expired' ? (
<Clock className="w-5 h-5 text-gray-400" />
) : (
<span className="text-[var(--app-brand)] font-bold">
{(binding.visitorNickname || '用户')[0]}
</span>
)}
</div>
<div>
<p className="text-white text-sm font-medium">
{binding.visitorNickname || binding.visitorPhone || '匿名用户'}
</p>
<p className="text-[var(--app-text-tertiary)] text-xs">
{new Date(binding.bindingTime).toLocaleDateString('zh-CN')}
</p>
</div>
</div>
<div className="text-right">
{binding.status === 'converted' ? (
<>
<p className="text-green-400 font-semibold">+¥{binding.commission?.toFixed(2)}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"> ¥{binding.orderAmount}</p>
</>
) : (
<span className={`text-xs px-2 py-1 rounded-full ${getBindingStatusStyle(binding.daysRemaining)}`}>
{getBindingStatusText(binding)}
</span>
)}
</div>
</div>
))
)}
</div>
</>
)}
</div>
{/* 专属链接 - 简化显示 */}
<div className="glass-card p-5 mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-white font-semibold"></h3>
<span className="text-[var(--app-brand)] text-sm font-mono bg-[var(--app-brand-light)] px-3 py-1.5 rounded-lg">
{user.referralCode}
</span>
</div>
<p className="text-[var(--app-text-tertiary)] text-xs">
<span className="text-[#FFD700]">5%</span><span className="text-[var(--app-brand)]">{distributorShare}%</span>
</p>
</div>
{/* 分享按钮 */}
<div className="space-y-3 mb-6">
<button
onClick={() => setShowPoster(true)}
className="w-full glass-card p-4 flex items-center gap-4 touch-feedback"
>
<div className="w-12 h-12 rounded-xl bg-[var(--ios-indigo)]/20 flex items-center justify-center">
<ImageIcon className="w-6 h-6 text-[var(--ios-indigo)]" />
</div>
<div className="flex-1 text-left">
<p className="text-white font-medium">广</p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</div>
<ArrowRight className="w-5 h-5 text-[var(--app-text-tertiary)]" />
</button>
<button
onClick={handleShareToWechat}
className="w-full glass-card p-4 flex items-center gap-4 touch-feedback"
>
<div className="w-12 h-12 rounded-xl bg-[#07C160]/20 flex items-center justify-center">
<MessageCircle className="w-6 h-6 text-[#07C160]" />
</div>
<div className="flex-1 text-left">
<p className="text-white font-medium"></p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
</div>
<ArrowRight className="w-5 h-5 text-[var(--app-text-tertiary)]" />
</button>
<button
onClick={handleShare}
className="w-full glass-card p-4 flex items-center gap-4 touch-feedback"
>
<div className="w-12 h-12 rounded-xl bg-[var(--app-bg-secondary)] flex items-center justify-center">
<Share2 className="w-6 h-6 text-[var(--app-text-secondary)]" />
</div>
<div className="flex-1 text-left">
<p className="text-white font-medium"></p>
<p className="text-[var(--app-text-tertiary)] text-xs">使</p>
</div>
<ArrowRight className="w-5 h-5 text-[var(--app-text-tertiary)]" />
</button>
</div>
{/* 收益明细 */}
{referralPurchases.length > 0 && (
<div className="glass-card overflow-hidden">
<div className="px-5 py-4 border-b border-[var(--app-separator)]">
<h3 className="text-white font-semibold"></h3>
</div>
<div className="max-h-60 overflow-auto scrollbar-hide">
{referralPurchases.slice(0, 10).map((purchase, idx) => (
<div
key={purchase.id}
className={`px-5 py-4 flex items-center justify-between ${idx !== referralPurchases.length - 1 ? 'border-b border-[var(--app-separator)]' : ''}`}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center">
<Gift className="w-5 h-5 text-[var(--app-brand)]" />
</div>
<div>
<p className="text-white text-sm font-medium">
{purchase.type === "fullbook" ? "整本书购买" : "单节购买"}
</p>
<p className="text-[var(--app-text-tertiary)] text-xs">
{new Date(purchase.createdAt).toLocaleDateString("zh-CN")}
</p>
</div>
</div>
<p className="text-[var(--app-brand)] font-semibold">
+¥{(purchase.referrerEarnings || 0).toFixed(2)}
</p>
</div>
))}
</div>
</div>
)}
{/* 空状态 */}
{referralPurchases.length === 0 && activeBindings.length === 0 && (
<div className="glass-card p-8 text-center">
<div className="w-16 h-16 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center mx-auto mb-4">
<Gift className="w-8 h-8 text-[var(--app-text-tertiary)]" />
</div>
<p className="text-white font-medium mb-2"></p>
<p className="text-[var(--app-text-tertiary)] text-sm">
{distributorShare}%
</p>
</div>
)}
</main>
<PosterModal
isOpen={showPoster}
onClose={() => setShowPoster(false)}
referralLink={referralLink}
referralCode={user.referralCode}
nickname={user.nickname}
/>
<WithdrawalModal
isOpen={showWithdrawal}
onClose={() => setShowWithdrawal(false)}
availableAmount={totalEarnings}
/>
<AutoWithdrawModal
isOpen={showAutoWithdraw}
onClose={() => setShowAutoWithdraw(false)}
enabled={autoWithdrawEnabled}
threshold={autoWithdrawThreshold}
onSave={(enabled, threshold, account) => {
setAutoWithdrawEnabled(enabled)
setAutoWithdrawThreshold(threshold)
// 实际调用API保存
}}
/>
</div>
)
}