feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API

主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

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

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

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

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
This commit is contained in:
卡若
2026-01-21 15:49:12 +08:00
parent 1ee25e3dab
commit b60edb3d47
197 changed files with 34430 additions and 7345 deletions

View File

@@ -2,18 +2,50 @@
import { useState, useEffect } from "react"
import Link from "next/link"
import { ChevronLeft, Copy, Share2, Users, Wallet, MessageCircle, ImageIcon, TrendingUp, Gift, Check, ArrowRight } from "lucide-react"
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) {
@@ -24,9 +56,77 @@ export default function ReferralPage() {
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">
@@ -82,6 +182,25 @@ export default function ReferralPage() {
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">
{/* 背景光效 */}
@@ -96,11 +215,38 @@ export default function ReferralPage() {
<ChevronLeft className="w-5 h-5 text-[var(--app-text-secondary)]" />
</Link>
<h1 className="flex-1 text-center font-semibold"></h1>
<div className="w-8" />
<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">
{/* 背景装饰 */}
@@ -123,57 +269,180 @@ export default function ReferralPage() {
</div>
</div>
<button
disabled={totalEarnings < 10}
onClick={() => setShowWithdrawal(true)}
className="btn-ios w-full glow disabled:opacity-50 disabled:cursor-not-allowed"
>
{totalEarnings < 10 ? `满10元可提现` : "申请提现"}
</button>
<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-2 gap-4 mb-6">
<div className="glass-card p-4 text-center">
<div className="w-10 h-10 rounded-xl bg-[var(--ios-blue)]/20 flex items-center justify-center mx-auto mb-2">
<Users className="w-5 h-5 text-[var(--ios-blue)]" />
</div>
<p className="text-2xl font-bold text-white mb-0.5">{referralUsers}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
<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-4 text-center">
<div className="w-10 h-10 rounded-xl bg-[var(--ios-purple)]/20 flex items-center justify-center mx-auto mb-2">
<TrendingUp className="w-5 h-5 text-[var(--ios-purple)]" />
</div>
<p className="text-2xl font-bold text-white mb-0.5">{referralPurchases.length}</p>
<p className="text-[var(--app-text-tertiary)] text-xs"></p>
<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-xs bg-[var(--app-brand-light)] px-2 py-1 rounded-full">
<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>
<div className="flex gap-2 mb-4">
<div className="flex-1 bg-[var(--app-bg-secondary)] rounded-xl px-4 py-3 text-[var(--app-text-secondary)] text-sm truncate font-mono">
{referralLink}
</div>
<button
onClick={handleCopy}
className="w-12 h-12 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center text-[var(--app-brand)] touch-feedback"
>
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
</button>
</div>
<p className="text-[var(--app-text-tertiary)] text-xs">
{distributorShare}%
<span className="text-[#FFD700]">5%</span><span className="text-[var(--app-brand)]">{distributorShare}%</span>
</p>
</div>
@@ -257,7 +526,7 @@ export default function ReferralPage() {
)}
{/* 空状态 */}
{referralPurchases.length === 0 && (
{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)]" />
@@ -283,6 +552,18 @@ export default function ReferralPage() {
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>
)
}