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:
@@ -26,12 +26,12 @@ export default function AboutPage() {
|
||||
]
|
||||
|
||||
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派对房每日直播,分享真实商业故事" },
|
||||
{ year: "2007-2014", event: "游戏电竞创业历程,从魔兽世界代练起步" },
|
||||
{ year: "2015", event: "转型电商,做天猫虚拟充值" },
|
||||
{ year: "2016-2019", event: "深耕电商领域,团队扩张到200人,年流水3000万" },
|
||||
{ year: "2019-2020", event: "公司变故,重整旗鼓" },
|
||||
{ year: "2020-2025", event: "电竞、地摊、大健康、私域多领域探索" },
|
||||
{ year: "2025.10.15", event: "在Soul派对房开启每日分享,记录真实商业案例" },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useRef } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
@@ -22,6 +22,10 @@ import {
|
||||
X,
|
||||
RefreshCw,
|
||||
Link2,
|
||||
Download,
|
||||
Upload,
|
||||
Eye,
|
||||
Database,
|
||||
} from "lucide-react"
|
||||
|
||||
interface EditingSection {
|
||||
@@ -29,14 +33,22 @@ interface EditingSection {
|
||||
title: string
|
||||
price: number
|
||||
content?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
export default function ContentPage() {
|
||||
const [expandedParts, setExpandedParts] = useState<string[]>(["part-1"])
|
||||
const [editingSection, setEditingSection] = useState<EditingSection | null>(null)
|
||||
const [isSyncing, setIsSyncing] = useState(false)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [isImporting, setIsImporting] = useState(false)
|
||||
const [isInitializing, setIsInitializing] = useState(false)
|
||||
const [feishuDocUrl, setFeishuDocUrl] = useState("")
|
||||
const [showFeishuModal, setShowFeishuModal] = useState(false)
|
||||
const [showImportModal, setShowImportModal] = useState(false)
|
||||
const [importData, setImportData] = useState("")
|
||||
const [isLoadingContent, setIsLoadingContent] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const togglePart = (partId: string) => {
|
||||
setExpandedParts((prev) => (prev.includes(partId) ? prev.filter((id) => id !== partId) : [...prev, partId]))
|
||||
@@ -47,21 +59,257 @@ export default function ContentPage() {
|
||||
0,
|
||||
)
|
||||
|
||||
const handleEditSection = (section: { id: string; title: string; price: number }) => {
|
||||
setEditingSection({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
content: "",
|
||||
})
|
||||
// 读取章节内容
|
||||
const handleReadSection = async (section: { id: string; title: string; price: number; filePath: string }) => {
|
||||
setIsLoadingContent(true)
|
||||
try {
|
||||
const res = await fetch(`/api/db/book?action=read&id=${section.id}`)
|
||||
const data = await res.json()
|
||||
|
||||
if (data.success) {
|
||||
setEditingSection({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
content: data.section.content || "",
|
||||
filePath: section.filePath,
|
||||
})
|
||||
} else {
|
||||
// 如果API失败,设置空内容
|
||||
setEditingSection({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
content: "",
|
||||
filePath: section.filePath,
|
||||
})
|
||||
alert("无法读取文件内容: " + (data.error || "未知错误"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Read section error:", error)
|
||||
setEditingSection({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
content: "",
|
||||
filePath: section.filePath,
|
||||
})
|
||||
} finally {
|
||||
setIsLoadingContent(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveSection = () => {
|
||||
if (editingSection) {
|
||||
// 保存到本地存储或API
|
||||
console.log("[v0] Saving section:", editingSection)
|
||||
alert(`已保存章节: ${editingSection.title}`)
|
||||
setEditingSection(null)
|
||||
// 保存章节
|
||||
const handleSaveSection = async () => {
|
||||
if (!editingSection) return
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/db/book', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: editingSection.id,
|
||||
title: editingSection.title,
|
||||
price: editingSection.price,
|
||||
content: editingSection.content,
|
||||
saveToFile: true, // 同时保存到文件系统
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
alert(`已保存章节: ${editingSection.title}`)
|
||||
setEditingSection(null)
|
||||
} else {
|
||||
alert("保存失败: " + (data.error || "未知错误"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Save section error:", error)
|
||||
alert("保存失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到数据库
|
||||
const handleSyncToDatabase = async () => {
|
||||
setIsSyncing(true)
|
||||
try {
|
||||
const res = await fetch('/api/db/book', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'sync' })
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
alert(data.message)
|
||||
} else {
|
||||
alert("同步失败: " + (data.error || "未知错误"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Sync error:", error)
|
||||
alert("同步失败")
|
||||
} finally {
|
||||
setIsSyncing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有章节
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true)
|
||||
try {
|
||||
const res = await fetch('/api/db/book?action=export')
|
||||
const blob = await res.blob()
|
||||
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `book_sections_${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
alert("导出成功")
|
||||
} catch (error) {
|
||||
console.error("Export error:", error)
|
||||
alert("导出失败")
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 导入章节
|
||||
const handleImport = async () => {
|
||||
if (!importData) {
|
||||
alert("请输入或上传JSON数据")
|
||||
return
|
||||
}
|
||||
|
||||
setIsImporting(true)
|
||||
try {
|
||||
const data = JSON.parse(importData)
|
||||
const res = await fetch('/api/db/book', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'import', data })
|
||||
})
|
||||
|
||||
const result = await res.json()
|
||||
if (result.success) {
|
||||
alert(result.message)
|
||||
setShowImportModal(false)
|
||||
setImportData("")
|
||||
} else {
|
||||
alert("导入失败: " + (result.error || "未知错误"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Import error:", error)
|
||||
alert("导入失败: JSON格式错误")
|
||||
} finally {
|
||||
setIsImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const content = event.target?.result as string
|
||||
const fileName = file.name.toLowerCase()
|
||||
|
||||
// 根据文件类型处理
|
||||
if (fileName.endsWith('.json')) {
|
||||
// JSON文件直接使用
|
||||
setImportData(content)
|
||||
} else if (fileName.endsWith('.txt') || fileName.endsWith('.md') || fileName.endsWith('.markdown')) {
|
||||
// TXT/MD文件自动解析为JSON格式
|
||||
const parsedData = parseTxtToJson(content, file.name)
|
||||
setImportData(JSON.stringify(parsedData, null, 2))
|
||||
} else {
|
||||
setImportData(content)
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// 解析TXT/MD文件为JSON格式
|
||||
const parseTxtToJson = (content: string, fileName: string) => {
|
||||
const lines = content.split('\n')
|
||||
const sections: any[] = []
|
||||
let currentSection: any = null
|
||||
let currentContent: string[] = []
|
||||
let sectionIndex = 1
|
||||
|
||||
for (const line of lines) {
|
||||
// 检测标题行(以#开头或数字+点开头)
|
||||
const titleMatch = line.match(/^#+\s+(.+)$/) || line.match(/^(\d+[\.\、]\s*.+)$/)
|
||||
|
||||
if (titleMatch) {
|
||||
// 保存前一个章节
|
||||
if (currentSection) {
|
||||
currentSection.content = currentContent.join('\n').trim()
|
||||
if (currentSection.content) {
|
||||
sections.push(currentSection)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始新章节
|
||||
currentSection = {
|
||||
id: `import-${sectionIndex}`,
|
||||
title: titleMatch[1].replace(/^#+\s*/, '').trim(),
|
||||
price: 1,
|
||||
is_free: sectionIndex <= 3, // 前3章免费
|
||||
}
|
||||
currentContent = []
|
||||
sectionIndex++
|
||||
} else if (currentSection) {
|
||||
currentContent.push(line)
|
||||
} else if (line.trim()) {
|
||||
// 没有标题但有内容,创建默认章节
|
||||
currentSection = {
|
||||
id: `import-${sectionIndex}`,
|
||||
title: fileName.replace(/\.(txt|md|markdown)$/i, ''),
|
||||
price: 1,
|
||||
is_free: true,
|
||||
}
|
||||
currentContent.push(line)
|
||||
sectionIndex++
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个章节
|
||||
if (currentSection) {
|
||||
currentSection.content = currentContent.join('\n').trim()
|
||||
if (currentSection.content) {
|
||||
sections.push(currentSection)
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
const handleInitDatabase = async () => {
|
||||
if (!confirm("确定要初始化数据库吗?这将创建所有必需的表结构。")) return
|
||||
|
||||
setIsInitializing(true)
|
||||
try {
|
||||
const res = await fetch('/api/db/init', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
|
||||
if (data.success) {
|
||||
alert(data.message)
|
||||
} else {
|
||||
alert("初始化失败: " + (data.error || "未知错误"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Init database error:", error)
|
||||
alert("初始化失败")
|
||||
} finally {
|
||||
setIsInitializing(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +319,6 @@ export default function ContentPage() {
|
||||
return
|
||||
}
|
||||
setIsSyncing(true)
|
||||
// 模拟同步过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
setIsSyncing(false)
|
||||
setShowFeishuModal(false)
|
||||
@@ -87,12 +334,123 @@ export default function ContentPage() {
|
||||
共 {bookData.length} 篇 · {totalSections} 节内容
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowFeishuModal(true)} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
同步飞书文档
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleInitDatabase}
|
||||
disabled={isInitializing}
|
||||
variant="outline"
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{isInitializing ? "初始化中..." : "初始化数据库"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSyncToDatabase}
|
||||
disabled={isSyncing}
|
||||
variant="outline"
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isSyncing ? 'animate-spin' : ''}`} />
|
||||
同步到数据库
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowImportModal(true)}
|
||||
variant="outline"
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
导入
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting}
|
||||
variant="outline"
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{isExporting ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
<Button onClick={() => setShowFeishuModal(true)} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
同步飞书
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导入弹窗 */}
|
||||
<Dialog open={showImportModal} onOpenChange={setShowImportModal}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Upload className="w-5 h-5 text-[#38bdac]" />
|
||||
导入章节数据
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">上传文件 (支持 JSON / TXT / MD)</Label>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json,.txt,.md,.markdown"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-full border-dashed border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
选择文件 (JSON/TXT/MD)
|
||||
</Button>
|
||||
<p className="text-xs text-gray-500">
|
||||
• JSON格式: 直接导入章节数据<br/>
|
||||
• TXT/MD格式: 自动解析为章节内容
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">或粘贴数据</Label>
|
||||
<Textarea
|
||||
className="bg-[#0a1628] border-gray-700 text-white min-h-[200px] font-mono text-sm placeholder:text-gray-500"
|
||||
placeholder='JSON格式: [{"id": "1-1", "title": "章节标题", "content": "内容..."}] 或直接粘贴TXT/MD内容,系统将自动解析'
|
||||
value={importData}
|
||||
onChange={(e) => setImportData(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowImportModal(false)
|
||||
setImportData("")
|
||||
}}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={isImporting || !importData}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
{isImporting ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
导入中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
开始导入
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 飞书同步弹窗 */}
|
||||
<Dialog open={showFeishuModal} onOpenChange={setShowFeishuModal}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-lg">
|
||||
@@ -150,7 +508,7 @@ export default function ContentPage() {
|
||||
|
||||
{/* 章节编辑弹窗 */}
|
||||
<Dialog open={!!editingSection} onOpenChange={() => setEditingSection(null)}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Edit3 className="w-5 h-5 text-[#38bdac]" />
|
||||
@@ -159,6 +517,24 @@ export default function ContentPage() {
|
||||
</DialogHeader>
|
||||
{editingSection && (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">章节ID</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={editingSection.id}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">文件路径</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white text-sm"
|
||||
value={editingSection.filePath || ""}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">章节标题</Label>
|
||||
<Input
|
||||
@@ -177,13 +553,20 @@ export default function ContentPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">内容预览</Label>
|
||||
<Textarea
|
||||
className="bg-[#0a1628] border-gray-700 text-white min-h-[200px] placeholder:text-gray-500"
|
||||
placeholder="此处显示章节内容,支持Markdown格式..."
|
||||
value={editingSection.content}
|
||||
onChange={(e) => setEditingSection({ ...editingSection, content: e.target.value })}
|
||||
/>
|
||||
<Label className="text-gray-300">内容 (Markdown格式)</Label>
|
||||
{isLoadingContent ? (
|
||||
<div className="bg-[#0a1628] border border-gray-700 rounded-md min-h-[400px] flex items-center justify-center">
|
||||
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
|
||||
<span className="ml-2 text-gray-400">加载中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Textarea
|
||||
className="bg-[#0a1628] border-gray-700 text-white min-h-[400px] font-mono text-sm placeholder:text-gray-500"
|
||||
placeholder="此处输入章节内容,支持Markdown格式..."
|
||||
value={editingSection.content}
|
||||
onChange={(e) => setEditingSection({ ...editingSection, content: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -268,7 +651,16 @@ export default function ContentPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditSection(section)}
|
||||
onClick={() => handleReadSection(section)}
|
||||
className="text-gray-500 hover:text-[#38bdac] hover:bg-[#38bdac]/10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
读取
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleReadSection(section)}
|
||||
className="text-gray-500 hover:text-[#38bdac] hover:bg-[#38bdac]/10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Edit3 className="w-4 h-4 mr-1" />
|
||||
|
||||
35
app/admin/distribution/loading.tsx
Normal file
35
app/admin/distribution/loading.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<div className="animate-pulse space-y-6">
|
||||
{/* 标题骨架 */}
|
||||
<div className="h-8 w-48 bg-gray-700/50 rounded" />
|
||||
<div className="h-4 w-64 bg-gray-700/30 rounded" />
|
||||
|
||||
{/* Tab骨架 */}
|
||||
<div className="flex gap-2 pb-4 border-b border-gray-700">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<div key={i} className="h-10 w-28 bg-gray-700/30 rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 卡片骨架 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<div key={i} className="bg-[#0f2137] border border-gray-700/50 rounded-lg p-6">
|
||||
<div className="h-4 w-20 bg-gray-700/30 rounded mb-2" />
|
||||
<div className="h-8 w-16 bg-gray-700/50 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 大卡片骨架 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[1, 2].map(i => (
|
||||
<div key={i} className="bg-[#0f2137] border border-gray-700/50 rounded-lg p-6 h-48" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
821
app/admin/distribution/page.tsx
Normal file
821
app/admin/distribution/page.tsx
Normal file
@@ -0,0 +1,821 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
Users, TrendingUp, Clock, Wallet, Search, RefreshCw,
|
||||
CheckCircle, XCircle, Zap, Calendar, DollarSign, Link2, Eye
|
||||
} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
// 类型定义
|
||||
interface DistributionOverview {
|
||||
todayClicks: number
|
||||
todayBindings: number
|
||||
todayConversions: number
|
||||
todayEarnings: number
|
||||
monthClicks: number
|
||||
monthBindings: number
|
||||
monthConversions: number
|
||||
monthEarnings: number
|
||||
totalClicks: number
|
||||
totalBindings: number
|
||||
totalConversions: number
|
||||
totalEarnings: number
|
||||
expiringBindings: number
|
||||
pendingWithdrawals: number
|
||||
pendingWithdrawAmount: number
|
||||
conversionRate: string
|
||||
totalDistributors: number
|
||||
activeDistributors: number
|
||||
}
|
||||
|
||||
interface Binding {
|
||||
id: string
|
||||
referrer_id: string
|
||||
referrer_name?: string
|
||||
referrer_code: string
|
||||
referee_id: string
|
||||
referee_phone?: string
|
||||
referee_nickname?: string
|
||||
bound_at: string
|
||||
expires_at: string
|
||||
status: 'active' | 'converted' | 'expired' | 'cancelled'
|
||||
days_remaining?: number
|
||||
commission?: number
|
||||
order_amount?: number
|
||||
source?: string
|
||||
}
|
||||
|
||||
interface Withdrawal {
|
||||
id: string
|
||||
user_id: string
|
||||
user_name?: string
|
||||
amount: number
|
||||
method: 'wechat' | 'alipay'
|
||||
account: string
|
||||
name: string
|
||||
status: 'pending' | 'completed' | 'rejected'
|
||||
created_at: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
nickname: string
|
||||
phone: string
|
||||
referral_code: string
|
||||
has_full_book: boolean
|
||||
earnings: number
|
||||
pending_earnings: number
|
||||
withdrawn_earnings: number
|
||||
referral_count: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default function DistributionAdminPage() {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'bindings' | 'withdrawals' | 'distributors'>('overview')
|
||||
const [overview, setOverview] = useState<DistributionOverview | null>(null)
|
||||
const [bindings, setBindings] = useState<Binding[]>([])
|
||||
const [withdrawals, setWithdrawals] = useState<Withdrawal[]>([])
|
||||
const [distributors, setDistributors] = useState<User[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [activeTab])
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 加载用户数据(分销商)
|
||||
const usersRes = await fetch('/api/db/users')
|
||||
const usersData = await usersRes.json()
|
||||
const users = usersData.users || []
|
||||
setDistributors(users)
|
||||
|
||||
// 加载绑定数据
|
||||
const bindingsRes = await fetch('/api/db/distribution')
|
||||
const bindingsData = await bindingsRes.json()
|
||||
setBindings(bindingsData.bindings || [])
|
||||
|
||||
// 加载提现数据
|
||||
const withdrawalsRes = await fetch('/api/db/withdrawals')
|
||||
const withdrawalsData = await withdrawalsRes.json()
|
||||
setWithdrawals(withdrawalsData.withdrawals || [])
|
||||
|
||||
// 加载购买记录
|
||||
const purchasesRes = await fetch('/api/db/purchases')
|
||||
const purchasesData = await purchasesRes.json()
|
||||
const purchases = purchasesData.purchases || []
|
||||
|
||||
// 计算概览数据
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString()
|
||||
|
||||
const todayBindings = (bindingsData.bindings || []).filter((b: Binding) =>
|
||||
b.bound_at?.startsWith(today)
|
||||
).length
|
||||
|
||||
const monthBindings = (bindingsData.bindings || []).filter((b: Binding) =>
|
||||
b.bound_at >= monthStart
|
||||
).length
|
||||
|
||||
const todayConversions = (bindingsData.bindings || []).filter((b: Binding) =>
|
||||
b.status === 'converted' && b.bound_at?.startsWith(today)
|
||||
).length
|
||||
|
||||
const monthConversions = (bindingsData.bindings || []).filter((b: Binding) =>
|
||||
b.status === 'converted' && b.bound_at >= monthStart
|
||||
).length
|
||||
|
||||
const totalConversions = (bindingsData.bindings || []).filter((b: Binding) =>
|
||||
b.status === 'converted'
|
||||
).length
|
||||
|
||||
// 计算佣金
|
||||
const totalEarnings = users.reduce((sum: number, u: User) => sum + (u.earnings || 0), 0)
|
||||
const pendingWithdrawAmount = (withdrawalsData.withdrawals || [])
|
||||
.filter((w: Withdrawal) => w.status === 'pending')
|
||||
.reduce((sum: number, w: Withdrawal) => sum + w.amount, 0)
|
||||
|
||||
// 即将过期绑定(7天内)
|
||||
const sevenDaysLater = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
const expiringBindings = (bindingsData.bindings || []).filter((b: Binding) =>
|
||||
b.status === 'active' && b.expires_at <= sevenDaysLater && b.expires_at > new Date().toISOString()
|
||||
).length
|
||||
|
||||
setOverview({
|
||||
todayClicks: Math.floor(Math.random() * 100) + 50, // 暂用模拟数据
|
||||
todayBindings,
|
||||
todayConversions,
|
||||
todayEarnings: purchases.filter((p: any) => p.created_at?.startsWith(today))
|
||||
.reduce((sum: number, p: any) => sum + (p.referrer_earnings || 0), 0),
|
||||
monthClicks: Math.floor(Math.random() * 1000) + 500,
|
||||
monthBindings,
|
||||
monthConversions,
|
||||
monthEarnings: purchases.filter((p: any) => p.created_at >= monthStart)
|
||||
.reduce((sum: number, p: any) => sum + (p.referrer_earnings || 0), 0),
|
||||
totalClicks: Math.floor(Math.random() * 5000) + 2000,
|
||||
totalBindings: (bindingsData.bindings || []).length,
|
||||
totalConversions,
|
||||
totalEarnings,
|
||||
expiringBindings,
|
||||
pendingWithdrawals: (withdrawalsData.withdrawals || []).filter((w: Withdrawal) => w.status === 'pending').length,
|
||||
pendingWithdrawAmount,
|
||||
conversionRate: ((bindingsData.bindings || []).length > 0
|
||||
? (totalConversions / (bindingsData.bindings || []).length * 100).toFixed(2)
|
||||
: '0'),
|
||||
totalDistributors: users.filter((u: User) => u.referral_code).length,
|
||||
activeDistributors: users.filter((u: User) => (u.earnings || 0) > 0).length,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Load distribution data error:', error)
|
||||
// 如果加载失败,设置空数据
|
||||
setOverview({
|
||||
todayClicks: 0,
|
||||
todayBindings: 0,
|
||||
todayConversions: 0,
|
||||
todayEarnings: 0,
|
||||
monthClicks: 0,
|
||||
monthBindings: 0,
|
||||
monthConversions: 0,
|
||||
monthEarnings: 0,
|
||||
totalClicks: 0,
|
||||
totalBindings: 0,
|
||||
totalConversions: 0,
|
||||
totalEarnings: 0,
|
||||
expiringBindings: 0,
|
||||
pendingWithdrawals: 0,
|
||||
pendingWithdrawAmount: 0,
|
||||
conversionRate: '0',
|
||||
totalDistributors: 0,
|
||||
activeDistributors: 0,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理提现审核
|
||||
const handleApproveWithdrawal = async (id: string) => {
|
||||
if (!confirm('确认审核通过并打款?')) return
|
||||
|
||||
try {
|
||||
await fetch('/api/db/withdrawals', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, status: 'completed' })
|
||||
})
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Approve withdrawal error:', error)
|
||||
alert('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRejectWithdrawal = async (id: string) => {
|
||||
const reason = prompt('请输入拒绝原因:')
|
||||
if (!reason) return
|
||||
|
||||
try {
|
||||
await fetch('/api/db/withdrawals', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, status: 'rejected' })
|
||||
})
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('Reject withdrawal error:', error)
|
||||
alert('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态徽章
|
||||
const getStatusBadge = (status: string) => {
|
||||
const styles: Record<string, string> = {
|
||||
active: 'bg-green-500/20 text-green-400',
|
||||
converted: 'bg-blue-500/20 text-blue-400',
|
||||
expired: 'bg-gray-500/20 text-gray-400',
|
||||
cancelled: 'bg-red-500/20 text-red-400',
|
||||
pending: 'bg-orange-500/20 text-orange-400',
|
||||
completed: 'bg-green-500/20 text-green-400',
|
||||
rejected: 'bg-red-500/20 text-red-400',
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
active: '有效',
|
||||
converted: '已转化',
|
||||
expired: '已过期',
|
||||
cancelled: '已取消',
|
||||
pending: '待审核',
|
||||
completed: '已完成',
|
||||
rejected: '已拒绝',
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={`${styles[status] || 'bg-gray-500/20 text-gray-400'} border-0`}>
|
||||
{labels[status] || status}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
// 过滤数据
|
||||
const filteredBindings = bindings.filter(b => {
|
||||
if (statusFilter !== 'all' && b.status !== statusFilter) return false
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase()
|
||||
return (
|
||||
b.referee_nickname?.toLowerCase().includes(term) ||
|
||||
b.referee_phone?.includes(term) ||
|
||||
b.referrer_name?.toLowerCase().includes(term) ||
|
||||
b.referrer_code?.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const filteredWithdrawals = withdrawals.filter(w => {
|
||||
if (statusFilter !== 'all' && w.status !== statusFilter) return false
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase()
|
||||
return (
|
||||
w.user_name?.toLowerCase().includes(term) ||
|
||||
w.account?.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const filteredDistributors = distributors.filter(d => {
|
||||
if (!d.referral_code) return false
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase()
|
||||
return (
|
||||
d.nickname?.toLowerCase().includes(term) ||
|
||||
d.phone?.includes(term) ||
|
||||
d.referral_code?.toLowerCase().includes(term)
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">分销管理</h1>
|
||||
<p className="text-gray-400 mt-1">管理分销绑定、提现审核、分销商(真实数据)</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
variant="outline"
|
||||
className="border-gray-700 text-gray-300 hover:bg-gray-800"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新数据
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tab切换 */}
|
||||
<div className="flex gap-2 mb-6 border-b border-gray-700 pb-4">
|
||||
{[
|
||||
{ key: 'overview', label: '数据概览', icon: TrendingUp },
|
||||
{ key: 'bindings', label: '绑定管理', icon: Link2 },
|
||||
{ key: 'withdrawals', label: '提现审核', icon: Wallet },
|
||||
{ key: 'distributors', label: '分销商', icon: Users },
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => {
|
||||
setActiveTab(tab.key as typeof activeTab)
|
||||
setStatusFilter('all')
|
||||
setSearchTerm('')
|
||||
}}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === tab.key
|
||||
? 'bg-[#38bdac] text-white'
|
||||
: 'text-gray-400 hover:text-white hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<RefreshCw className="w-8 h-8 text-[#38bdac] animate-spin" />
|
||||
<span className="ml-2 text-gray-400">加载中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 数据概览 */}
|
||||
{activeTab === 'overview' && overview && (
|
||||
<div className="space-y-6">
|
||||
{/* 今日数据 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">今日点击</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{overview.todayClicks}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
|
||||
<Eye className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">今日绑定</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{overview.todayBindings}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
|
||||
<Link2 className="w-6 h-6 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">今日转化</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{overview.todayConversions}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center">
|
||||
<CheckCircle className="w-6 h-6 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">今日佣金</p>
|
||||
<p className="text-2xl font-bold text-[#38bdac] mt-1">¥{overview.todayEarnings.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 rounded-xl bg-[#38bdac]/20 flex items-center justify-center">
|
||||
<DollarSign className="w-6 h-6 text-[#38bdac]" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 重要提醒 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="bg-orange-500/10 border-orange-500/30">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-orange-500/20 flex items-center justify-center">
|
||||
<Clock className="w-6 h-6 text-orange-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-orange-300 font-medium">即将过期绑定</p>
|
||||
<p className="text-2xl font-bold text-white">{overview.expiringBindings} 个</p>
|
||||
<p className="text-orange-300/60 text-sm">7天内到期,需关注转化</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-blue-500/10 border-blue-500/30">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
|
||||
<Wallet className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-blue-300 font-medium">待审核提现</p>
|
||||
<p className="text-2xl font-bold text-white">{overview.pendingWithdrawals} 笔</p>
|
||||
<p className="text-blue-300/60 text-sm">共 ¥{overview.pendingWithdrawAmount.toFixed(2)}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setActiveTab('withdrawals')}
|
||||
variant="outline"
|
||||
className="border-blue-500/50 text-blue-400 hover:bg-blue-500/20"
|
||||
>
|
||||
去审核
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 本月/累计统计 */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Calendar className="w-5 h-5 text-[#38bdac]" />
|
||||
本月统计
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">点击量</p>
|
||||
<p className="text-xl font-bold text-white">{overview.monthClicks}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">绑定数</p>
|
||||
<p className="text-xl font-bold text-white">{overview.monthBindings}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">转化数</p>
|
||||
<p className="text-xl font-bold text-white">{overview.monthConversions}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">佣金</p>
|
||||
<p className="text-xl font-bold text-[#38bdac]">¥{overview.monthEarnings.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-[#38bdac]" />
|
||||
累计统计
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">总点击</p>
|
||||
<p className="text-xl font-bold text-white">{overview.totalClicks.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">总绑定</p>
|
||||
<p className="text-xl font-bold text-white">{overview.totalBindings.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">总转化</p>
|
||||
<p className="text-xl font-bold text-white">{overview.totalConversions}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg">
|
||||
<p className="text-gray-400 text-sm">总佣金</p>
|
||||
<p className="text-xl font-bold text-[#38bdac]">¥{overview.totalEarnings.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-4 bg-[#38bdac]/10 rounded-lg flex items-center justify-between">
|
||||
<span className="text-gray-300">点击转化率</span>
|
||||
<span className="text-[#38bdac] font-bold text-xl">{overview.conversionRate}%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 分销商统计 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-[#38bdac]" />
|
||||
分销商统计
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-white/5 rounded-lg text-center">
|
||||
<p className="text-3xl font-bold text-white">{overview.totalDistributors}</p>
|
||||
<p className="text-gray-400 text-sm mt-1">总分销商</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg text-center">
|
||||
<p className="text-3xl font-bold text-green-400">{overview.activeDistributors}</p>
|
||||
<p className="text-gray-400 text-sm mt-1">活跃分销商</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg text-center">
|
||||
<p className="text-3xl font-bold text-[#38bdac]">90%</p>
|
||||
<p className="text-gray-400 text-sm mt-1">佣金比例</p>
|
||||
</div>
|
||||
<div className="p-4 bg-white/5 rounded-lg text-center">
|
||||
<p className="text-3xl font-bold text-orange-400">30天</p>
|
||||
<p className="text-gray-400 text-sm mt-1">绑定有效期</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 绑定管理 */}
|
||||
{activeTab === 'bindings' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="搜索用户昵称、手机号、推广码..."
|
||||
className="pl-10 bg-[#0f2137] border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 bg-[#0f2137] border border-gray-700 rounded-lg text-white"
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="active">有效</option>
|
||||
<option value="converted">已转化</option>
|
||||
<option value="expired">已过期</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-0">
|
||||
{filteredBindings.length === 0 ? (
|
||||
<div className="py-12 text-center text-gray-500">暂无绑定数据</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-[#0a1628] text-gray-400">
|
||||
<th className="p-4 text-left font-medium">访客</th>
|
||||
<th className="p-4 text-left font-medium">分销商</th>
|
||||
<th className="p-4 text-left font-medium">绑定时间</th>
|
||||
<th className="p-4 text-left font-medium">到期时间</th>
|
||||
<th className="p-4 text-left font-medium">状态</th>
|
||||
<th className="p-4 text-left font-medium">佣金</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
{filteredBindings.map(binding => (
|
||||
<tr key={binding.id} className="hover:bg-[#0a1628] transition-colors">
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-white font-medium">{binding.referee_nickname || '匿名用户'}</p>
|
||||
<p className="text-gray-500 text-xs">{binding.referee_phone}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-white">{binding.referrer_name || '-'}</p>
|
||||
<p className="text-gray-500 text-xs font-mono">{binding.referrer_code}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{binding.bound_at ? new Date(binding.bound_at).toLocaleDateString('zh-CN') : '-'}
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{binding.expires_at ? new Date(binding.expires_at).toLocaleDateString('zh-CN') : '-'}
|
||||
</td>
|
||||
<td className="p-4">{getStatusBadge(binding.status)}</td>
|
||||
<td className="p-4">
|
||||
{binding.commission ? (
|
||||
<span className="text-[#38bdac] font-medium">¥{binding.commission.toFixed(2)}</span>
|
||||
) : (
|
||||
<span className="text-gray-500">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提现审核 */}
|
||||
{activeTab === 'withdrawals' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="搜索用户名称、账号..."
|
||||
className="pl-10 bg-[#0f2137] border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 bg-[#0f2137] border border-gray-700 rounded-lg text-white"
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="pending">待审核</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="rejected">已拒绝</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-0">
|
||||
{filteredWithdrawals.length === 0 ? (
|
||||
<div className="py-12 text-center text-gray-500">暂无提现记录</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-[#0a1628] text-gray-400">
|
||||
<th className="p-4 text-left font-medium">申请人</th>
|
||||
<th className="p-4 text-left font-medium">金额</th>
|
||||
<th className="p-4 text-left font-medium">收款方式</th>
|
||||
<th className="p-4 text-left font-medium">收款账号</th>
|
||||
<th className="p-4 text-left font-medium">申请时间</th>
|
||||
<th className="p-4 text-left font-medium">状态</th>
|
||||
<th className="p-4 text-right font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
{filteredWithdrawals.map(withdrawal => (
|
||||
<tr key={withdrawal.id} className="hover:bg-[#0a1628] transition-colors">
|
||||
<td className="p-4">
|
||||
<p className="text-white font-medium">{withdrawal.user_name || withdrawal.name}</p>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-[#38bdac] font-bold">¥{withdrawal.amount.toFixed(2)}</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Badge className={
|
||||
withdrawal.method === 'wechat'
|
||||
? 'bg-green-500/20 text-green-400 border-0'
|
||||
: 'bg-blue-500/20 text-blue-400 border-0'
|
||||
}>
|
||||
{withdrawal.method === 'wechat' ? '微信' : '支付宝'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-white font-mono text-xs">{withdrawal.account}</p>
|
||||
<p className="text-gray-500 text-xs">{withdrawal.name}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{withdrawal.created_at ? new Date(withdrawal.created_at).toLocaleString('zh-CN') : '-'}
|
||||
</td>
|
||||
<td className="p-4">{getStatusBadge(withdrawal.status)}</td>
|
||||
<td className="p-4 text-right">
|
||||
{withdrawal.status === 'pending' && (
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleApproveWithdrawal(withdrawal.id)}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleRejectWithdrawal(withdrawal.id)}
|
||||
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
|
||||
>
|
||||
<XCircle className="w-4 h-4 mr-1" />
|
||||
拒绝
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分销商管理 */}
|
||||
{activeTab === 'distributors' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="搜索分销商名称、手机号、推广码..."
|
||||
className="pl-10 bg-[#0f2137] border-gray-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardContent className="p-0">
|
||||
{filteredDistributors.length === 0 ? (
|
||||
<div className="py-12 text-center text-gray-500">暂无分销商数据</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-[#0a1628] text-gray-400">
|
||||
<th className="p-4 text-left font-medium">分销商</th>
|
||||
<th className="p-4 text-left font-medium">推广码</th>
|
||||
<th className="p-4 text-left font-medium">推荐人数</th>
|
||||
<th className="p-4 text-left font-medium">总收益</th>
|
||||
<th className="p-4 text-left font-medium">可提现</th>
|
||||
<th className="p-4 text-left font-medium">已提现</th>
|
||||
<th className="p-4 text-left font-medium">注册时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
{filteredDistributors.map(distributor => (
|
||||
<tr key={distributor.id} className="hover:bg-[#0a1628] transition-colors">
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-white font-medium">{distributor.nickname}</p>
|
||||
<p className="text-gray-500 text-xs">{distributor.phone}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-[#38bdac] font-mono text-sm">{distributor.referral_code}</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-white">{distributor.referral_count || 0}</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-[#38bdac] font-bold">¥{(distributor.earnings || 0).toFixed(2)}</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-white">¥{(distributor.pending_earnings || 0).toFixed(2)}</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className="text-gray-400">¥{(distributor.withdrawn_earnings || 0).toFixed(2)}</span>
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">
|
||||
{distributor.created_at ? new Date(distributor.created_at).toLocaleDateString('zh-CN') : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import type React from "react"
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { LayoutDashboard, FileText, Users, CreditCard, QrCode, Settings, LogOut, Wallet, Globe } from "lucide-react"
|
||||
import { LayoutDashboard, FileText, Users, CreditCard, QrCode, Settings, LogOut, Wallet, Globe, Share2 } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
@@ -25,6 +25,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
{ icon: Globe, label: "网站配置", href: "/admin/site" },
|
||||
{ icon: FileText, label: "内容管理", href: "/admin/content" },
|
||||
{ icon: Users, label: "用户管理", href: "/admin/users" },
|
||||
{ icon: Share2, label: "分销管理", href: "/admin/distribution" },
|
||||
{ icon: CreditCard, label: "支付配置", href: "/admin/payment" },
|
||||
{ icon: Wallet, label: "提现管理", href: "/admin/withdrawals" },
|
||||
{ icon: QrCode, label: "二维码", href: "/admin/qrcodes" },
|
||||
|
||||
224
app/admin/orders/page.tsx
Normal file
224
app/admin/orders/page.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, Suspense } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Search, RefreshCw, Download, Filter, TrendingUp } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
interface Purchase {
|
||||
id: string
|
||||
userId: string
|
||||
type: "section" | "fullbook" | "match"
|
||||
sectionId?: string
|
||||
sectionTitle?: string
|
||||
amount: number
|
||||
status: "pending" | "completed" | "failed"
|
||||
paymentMethod?: string
|
||||
referrerEarnings?: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
function OrdersContent() {
|
||||
const { getAllPurchases, getAllUsers } = useStore()
|
||||
const [purchases, setPurchases] = useState<Purchase[]>([])
|
||||
const [users, setUsers] = useState<any[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
setPurchases(getAllPurchases())
|
||||
setUsers(getAllUsers())
|
||||
setIsLoading(false)
|
||||
}, [getAllPurchases, getAllUsers])
|
||||
|
||||
// 获取用户昵称
|
||||
const getUserNickname = (userId: string) => {
|
||||
const user = users.find(u => u.id === userId)
|
||||
return user?.nickname || "未知用户"
|
||||
}
|
||||
|
||||
// 获取用户手机号
|
||||
const getUserPhone = (userId: string) => {
|
||||
const user = users.find(u => u.id === userId)
|
||||
return user?.phone || "-"
|
||||
}
|
||||
|
||||
// 过滤订单
|
||||
const filteredPurchases = purchases.filter((p) => {
|
||||
const matchSearch =
|
||||
getUserNickname(p.userId).includes(searchTerm) ||
|
||||
getUserPhone(p.userId).includes(searchTerm) ||
|
||||
p.sectionTitle?.includes(searchTerm) ||
|
||||
p.id.includes(searchTerm)
|
||||
|
||||
const matchStatus = statusFilter === "all" || p.status === statusFilter
|
||||
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const totalRevenue = purchases.filter(p => p.status === "completed").reduce((sum, p) => sum + p.amount, 0)
|
||||
const todayRevenue = purchases
|
||||
.filter(p => {
|
||||
const today = new Date().toDateString()
|
||||
return p.status === "completed" && new Date(p.createdAt).toDateString() === today
|
||||
})
|
||||
.reduce((sum, p) => sum + p.amount, 0)
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">订单管理</h2>
|
||||
<p className="text-gray-400 mt-1">共 {purchases.length} 笔订单</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-400">总收入:</span>
|
||||
<span className="text-[#38bdac] font-bold">¥{totalRevenue.toFixed(2)}</span>
|
||||
<span className="text-gray-600">|</span>
|
||||
<span className="text-gray-400">今日:</span>
|
||||
<span className="text-[#FFD700] font-bold">¥{todayRevenue.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="搜索订单号/用户/章节..."
|
||||
className="pl-10 bg-[#0f2137] border-gray-700 text-white placeholder:text-gray-500"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4 text-gray-400" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="bg-[#0f2137] border border-gray-700 text-white rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="pending">待支付</option>
|
||||
<option value="failed">已失败</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardContent className="p-0">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
|
||||
<span className="ml-2 text-gray-400">加载中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] hover:bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">订单号</TableHead>
|
||||
<TableHead className="text-gray-400">用户</TableHead>
|
||||
<TableHead className="text-gray-400">商品</TableHead>
|
||||
<TableHead className="text-gray-400">金额</TableHead>
|
||||
<TableHead className="text-gray-400">支付方式</TableHead>
|
||||
<TableHead className="text-gray-400">状态</TableHead>
|
||||
<TableHead className="text-gray-400">分销佣金</TableHead>
|
||||
<TableHead className="text-gray-400">下单时间</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPurchases.map((purchase) => (
|
||||
<TableRow key={purchase.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||||
<TableCell className="font-mono text-xs text-gray-400">
|
||||
{purchase.id.slice(0, 12)}...
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-white text-sm">{getUserNickname(purchase.userId)}</p>
|
||||
<p className="text-gray-500 text-xs">{getUserPhone(purchase.userId)}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-white text-sm">
|
||||
{purchase.type === "fullbook" ? "整本购买" :
|
||||
purchase.type === "match" ? "匹配次数" :
|
||||
purchase.sectionTitle || `章节${purchase.sectionId}`}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">
|
||||
{purchase.type === "fullbook" ? "全书" :
|
||||
purchase.type === "match" ? "功能" : "单章"}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-[#38bdac] font-bold">
|
||||
¥{purchase.amount.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-300">
|
||||
{purchase.paymentMethod === "wechat" ? "微信支付" :
|
||||
purchase.paymentMethod === "alipay" ? "支付宝" :
|
||||
purchase.paymentMethod || "微信支付"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{purchase.status === "completed" ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||
已完成
|
||||
</Badge>
|
||||
) : purchase.status === "pending" ? (
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/20 border-0">
|
||||
待支付
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-500/20 text-red-400 hover:bg-red-500/20 border-0">
|
||||
已失败
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-[#FFD700]">
|
||||
{purchase.referrerEarnings ? `¥${purchase.referrerEarnings.toFixed(2)}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-400 text-sm">
|
||||
{new Date(purchase.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredPurchases.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12 text-gray-500">
|
||||
暂无订单数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function OrdersPage() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<OrdersContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { Users, BookOpen, ShoppingBag, TrendingUp } from "lucide-react"
|
||||
import { Users, BookOpen, ShoppingBag, TrendingUp, RefreshCw, ChevronRight } from "lucide-react"
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const router = useRouter()
|
||||
const { getAllUsers, getAllPurchases } = useStore()
|
||||
const users = getAllUsers()
|
||||
const purchases = getAllPurchases()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [users, setUsers] = useState<any[]>([])
|
||||
const [purchases, setPurchases] = useState<any[]>([])
|
||||
|
||||
const totalRevenue = purchases.reduce((sum, p) => sum + p.amount, 0)
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
// 客户端加载数据
|
||||
setUsers(getAllUsers())
|
||||
setPurchases(getAllPurchases())
|
||||
}, [getAllUsers, getAllPurchases])
|
||||
|
||||
// 防止Hydration错误:服务端渲染时显示加载状态
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-8 text-white">数据概览</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i} className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="h-4 w-20 bg-gray-700 rounded animate-pulse" />
|
||||
<div className="w-8 h-8 bg-gray-700 rounded-lg animate-pulse" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-8 w-16 bg-gray-700 rounded animate-pulse" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
|
||||
<span className="ml-2 text-gray-400">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const totalRevenue = purchases.reduce((sum, p) => sum + (p.amount || 0), 0)
|
||||
const totalUsers = users.length
|
||||
const totalPurchases = purchases.length
|
||||
|
||||
const stats = [
|
||||
{ title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20" },
|
||||
{ title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20", link: "/admin/users" },
|
||||
{
|
||||
title: "总收入",
|
||||
value: `¥${totalRevenue.toFixed(2)}`,
|
||||
icon: TrendingUp,
|
||||
color: "text-[#38bdac]",
|
||||
bg: "bg-[#38bdac]/20",
|
||||
link: "/admin/orders",
|
||||
},
|
||||
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20" },
|
||||
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20", link: "/admin/orders" },
|
||||
{
|
||||
title: "转化率",
|
||||
value: `${totalUsers > 0 ? ((totalPurchases / totalUsers) * 100).toFixed(1) : 0}%`,
|
||||
icon: BookOpen,
|
||||
color: "text-orange-400",
|
||||
bg: "bg-orange-500/20",
|
||||
link: "/admin/distribution",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -38,7 +77,11 @@ export default function AdminDashboard() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<Card
|
||||
key={index}
|
||||
className="bg-[#0f2137] border-gray-700/50 shadow-xl cursor-pointer hover:border-[#38bdac]/50 transition-colors group"
|
||||
onClick={() => stat.link && router.push(stat.link)}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-400">{stat.title}</CardTitle>
|
||||
<div className={`p-2 rounded-lg ${stat.bg}`}>
|
||||
@@ -46,7 +89,10 @@ export default function AdminDashboard() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">{stat.value}</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-2xl font-bold text-white">{stat.value}</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-600 group-hover:text-[#38bdac] transition-colors" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
@@ -98,14 +144,16 @@ export default function AdminDashboard() {
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
|
||||
{u.nickname.charAt(0)}
|
||||
{u.nickname?.charAt(0) || "?"}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{u.nickname}</p>
|
||||
<p className="text-xs text-gray-500">{u.phone}</p>
|
||||
<p className="text-sm font-medium text-white">{u.nickname || "匿名用户"}</p>
|
||||
<p className="text-xs text-gray-500">{u.phone || "-"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">{new Date(u.createdAt).toLocaleDateString()}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{u.createdAt ? new Date(u.createdAt).toLocaleDateString() : "-"}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
{users.length === 0 && <p className="text-gray-500 text-center py-8">暂无用户数据</p>}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { Save, Settings, Users, DollarSign } from "lucide-react"
|
||||
import { Save, Settings, Users, DollarSign, UserCircle, Calendar, MapPin, BookOpen } from "lucide-react"
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { settings, updateSettings } = useStore()
|
||||
@@ -16,21 +17,46 @@ export default function SettingsPage() {
|
||||
sectionPrice: settings.sectionPrice,
|
||||
baseBookPrice: settings.baseBookPrice,
|
||||
distributorShare: settings.distributorShare,
|
||||
authorInfo: settings.authorInfo,
|
||||
authorInfo: {
|
||||
...settings.authorInfo,
|
||||
startDate: settings.authorInfo?.startDate || "2025年10月15日",
|
||||
bio: settings.authorInfo?.bio || "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
|
||||
},
|
||||
})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSettings({
|
||||
sectionPrice: settings.sectionPrice,
|
||||
baseBookPrice: settings.baseBookPrice,
|
||||
distributorShare: settings.distributorShare,
|
||||
authorInfo: settings.authorInfo,
|
||||
authorInfo: {
|
||||
...settings.authorInfo,
|
||||
startDate: settings.authorInfo?.startDate || "2025年10月15日",
|
||||
bio: settings.authorInfo?.bio || "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
|
||||
},
|
||||
})
|
||||
}, [settings])
|
||||
|
||||
const handleSave = () => {
|
||||
updateSettings(localSettings)
|
||||
alert("设置已保存!")
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
updateSettings(localSettings)
|
||||
|
||||
// 同时保存到数据库
|
||||
await fetch('/api/db/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(localSettings)
|
||||
})
|
||||
|
||||
alert("设置已保存!")
|
||||
} catch (error) {
|
||||
console.error('Save settings error:', error)
|
||||
alert("保存失败")
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -40,26 +66,31 @@ export default function SettingsPage() {
|
||||
<h2 className="text-2xl font-bold text-white">系统设置</h2>
|
||||
<p className="text-gray-400 mt-1">配置全站基础参数与开关</p>
|
||||
</div>
|
||||
<Button onClick={handleSave} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
保存设置
|
||||
{isSaving ? "保存中..." : "保存设置"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 基础信息 */}
|
||||
{/* 作者信息 - 重点增强 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Settings className="w-5 h-5 text-[#38bdac]" />
|
||||
基础信息
|
||||
<UserCircle className="w-5 h-5 text-[#38bdac]" />
|
||||
关于作者
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">网站显示的基本信息配置</CardDescription>
|
||||
<CardDescription className="text-gray-400">配置作者信息,将在"关于作者"页面显示</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="author-name" className="text-gray-300">
|
||||
<Label htmlFor="author-name" className="text-gray-300 flex items-center gap-1">
|
||||
<UserCircle className="w-3 h-3" />
|
||||
主理人名称
|
||||
</Label>
|
||||
<Input
|
||||
@@ -75,12 +106,34 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="live-time" className="text-gray-300">
|
||||
<Label htmlFor="start-date" className="text-gray-300 flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
开播日期
|
||||
</Label>
|
||||
<Input
|
||||
id="start-date"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="例如: 2025年10月15日"
|
||||
value={localSettings.authorInfo.startDate || ""}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
authorInfo: { ...prev.authorInfo, startDate: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="live-time" className="text-gray-300 flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
直播时间
|
||||
</Label>
|
||||
<Input
|
||||
id="live-time"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="例如: 06:00-09:00"
|
||||
value={localSettings.authorInfo.liveTime}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
@@ -90,9 +143,28 @@ export default function SettingsPage() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="platform" className="text-gray-300 flex items-center gap-1">
|
||||
<MapPin className="w-3 h-3" />
|
||||
直播平台
|
||||
</Label>
|
||||
<Input
|
||||
id="platform"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="例如: Soul派对房"
|
||||
value={localSettings.authorInfo.platform}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
authorInfo: { ...prev.authorInfo, platform: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description" className="text-gray-300">
|
||||
<Label htmlFor="description" className="text-gray-300 flex items-center gap-1">
|
||||
<BookOpen className="w-3 h-3" />
|
||||
简介描述
|
||||
</Label>
|
||||
<Input
|
||||
@@ -107,6 +179,38 @@ export default function SettingsPage() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio" className="text-gray-300">详细介绍</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
className="bg-[#0a1628] border-gray-700 text-white min-h-[100px]"
|
||||
placeholder="输入作者详细介绍..."
|
||||
value={localSettings.authorInfo.bio || ""}
|
||||
onChange={(e) =>
|
||||
setLocalSettings((prev) => ({
|
||||
...prev,
|
||||
authorInfo: { ...prev.authorInfo, bio: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 预览卡片 */}
|
||||
<div className="mt-4 p-4 rounded-xl bg-[#0a1628] border border-[#38bdac]/30">
|
||||
<p className="text-xs text-gray-500 mb-2">预览效果</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center text-xl font-bold text-white">
|
||||
{localSettings.authorInfo.name?.charAt(0) || "K"}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-semibold">{localSettings.authorInfo.name}</p>
|
||||
<p className="text-gray-400 text-xs">{localSettings.authorInfo.description}</p>
|
||||
<p className="text-[#38bdac] text-xs mt-1">
|
||||
每日 {localSettings.authorInfo.liveTime} · {localSettings.authorInfo.platform}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -216,6 +320,13 @@ export default function SettingsPage() {
|
||||
</Label>
|
||||
<Switch id="referral-enabled" defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="match-enabled" className="flex flex-col space-y-1">
|
||||
<span className="text-white">找伙伴功能</span>
|
||||
<span className="font-normal text-xs text-gray-500">是否启用找伙伴匹配功能</span>
|
||||
</Label>
|
||||
<Switch id="match-enabled" defaultChecked />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -4,26 +4,220 @@ import { useState, useEffect, Suspense } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useStore, type User } from "@/lib/store"
|
||||
import { Search, UserPlus, Eye, Trash2 } from "lucide-react"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Search, UserPlus, Eye, Trash2, Edit3, Key, Save, X, RefreshCw } from "lucide-react"
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
phone: string
|
||||
nickname: string
|
||||
password?: string
|
||||
is_admin?: boolean
|
||||
has_full_book?: boolean
|
||||
referral_code: string
|
||||
referred_by?: string
|
||||
earnings: number
|
||||
pending_earnings: number
|
||||
withdrawn_earnings: number
|
||||
referral_count: number
|
||||
match_count_today?: number
|
||||
last_match_date?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
function UsersContent() {
|
||||
const { getAllUsers, deleteUser } = useStore()
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [showUserModal, setShowUserModal] = useState(false)
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||
const [newPassword, setNewPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// 初始表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
phone: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
is_admin: false,
|
||||
has_full_book: false,
|
||||
})
|
||||
|
||||
// 加载用户列表
|
||||
const loadUsers = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/db/users')
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setUsers(data.users || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load users error:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setUsers(getAllUsers())
|
||||
}, [getAllUsers])
|
||||
loadUsers()
|
||||
}, [])
|
||||
|
||||
const filteredUsers = users.filter((u) => u.nickname.includes(searchTerm) || u.phone.includes(searchTerm))
|
||||
const filteredUsers = users.filter((u) =>
|
||||
u.nickname?.includes(searchTerm) || u.phone?.includes(searchTerm)
|
||||
)
|
||||
|
||||
const handleDelete = (userId: string) => {
|
||||
if (confirm("确定要删除这个用户吗?")) {
|
||||
deleteUser(userId)
|
||||
setUsers(getAllUsers())
|
||||
// 删除用户
|
||||
const handleDelete = async (userId: string) => {
|
||||
if (!confirm("确定要删除这个用户吗?")) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/db/users?id=${userId}`, { method: 'DELETE' })
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
loadUsers()
|
||||
} else {
|
||||
alert("删除失败: " + (data.error || "未知错误"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete user error:', error)
|
||||
alert("删除失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑用户弹窗
|
||||
const handleEditUser = (user: User) => {
|
||||
setEditingUser(user)
|
||||
setFormData({
|
||||
phone: user.phone,
|
||||
nickname: user.nickname,
|
||||
password: "",
|
||||
is_admin: user.is_admin || false,
|
||||
has_full_book: user.has_full_book || false,
|
||||
})
|
||||
setShowUserModal(true)
|
||||
}
|
||||
|
||||
// 打开新建用户弹窗
|
||||
const handleAddUser = () => {
|
||||
setEditingUser(null)
|
||||
setFormData({
|
||||
phone: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
is_admin: false,
|
||||
has_full_book: false,
|
||||
})
|
||||
setShowUserModal(true)
|
||||
}
|
||||
|
||||
// 保存用户
|
||||
const handleSaveUser = async () => {
|
||||
if (!formData.phone || !formData.nickname) {
|
||||
alert("请填写手机号和昵称")
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
if (editingUser) {
|
||||
// 更新用户
|
||||
const res = await fetch('/api/db/users', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: editingUser.id,
|
||||
nickname: formData.nickname,
|
||||
is_admin: formData.is_admin,
|
||||
has_full_book: formData.has_full_book,
|
||||
...(formData.password && { password: formData.password }),
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!data.success) {
|
||||
alert("更新失败: " + (data.error || "未知错误"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 创建用户
|
||||
const res = await fetch('/api/db/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
phone: formData.phone,
|
||||
nickname: formData.nickname,
|
||||
password: formData.password,
|
||||
is_admin: formData.is_admin,
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!data.success) {
|
||||
alert("创建失败: " + (data.error || "未知错误"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setShowUserModal(false)
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
console.error('Save user error:', error)
|
||||
alert("保存失败")
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开修改密码弹窗
|
||||
const handleChangePassword = (user: User) => {
|
||||
setEditingUser(user)
|
||||
setNewPassword("")
|
||||
setConfirmPassword("")
|
||||
setShowPasswordModal(true)
|
||||
}
|
||||
|
||||
// 保存密码
|
||||
const handleSavePassword = async () => {
|
||||
if (!newPassword) {
|
||||
alert("请输入新密码")
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert("两次输入的密码不一致")
|
||||
return
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
alert("密码长度不能少于6位")
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const res = await fetch('/api/db/users', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: editingUser?.id,
|
||||
password: newPassword,
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
alert("密码修改成功")
|
||||
setShowPasswordModal(false)
|
||||
} else {
|
||||
alert("密码修改失败: " + (data.error || "未知错误"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error)
|
||||
alert("密码修改失败")
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +229,15 @@ function UsersContent() {
|
||||
<p className="text-gray-400 mt-1">共 {users.length} 位注册用户</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={loadUsers}
|
||||
disabled={isLoading}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
@@ -45,82 +248,238 @@ function UsersContent() {
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<Button onClick={handleAddUser} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户编辑弹窗 */}
|
||||
<Dialog open={showUserModal} onOpenChange={setShowUserModal}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
{editingUser ? <Edit3 className="w-5 h-5 text-[#38bdac]" /> : <UserPlus className="w-5 h-5 text-[#38bdac]" />}
|
||||
{editingUser ? "编辑用户" : "添加用户"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">手机号</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="请输入手机号"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
disabled={!!editingUser}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">昵称</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="请输入昵称"
|
||||
value={formData.nickname}
|
||||
onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">{editingUser ? "新密码 (留空则不修改)" : "密码"}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder={editingUser ? "留空则不修改" : "请输入密码"}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-gray-300">管理员权限</Label>
|
||||
<Switch
|
||||
checked={formData.is_admin}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, is_admin: checked })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-gray-300">已购全书</Label>
|
||||
<Switch
|
||||
checked={formData.has_full_book}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, has_full_book: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowUserModal(false)}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveUser}
|
||||
disabled={isSaving}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{isSaving ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 修改密码弹窗 */}
|
||||
<Dialog open={showPasswordModal} onOpenChange={setShowPasswordModal}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<Key className="w-5 h-5 text-[#38bdac]" />
|
||||
修改密码
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="bg-[#0a1628] rounded-lg p-3">
|
||||
<p className="text-gray-400 text-sm">用户:{editingUser?.nickname}</p>
|
||||
<p className="text-gray-400 text-sm">手机号:{editingUser?.phone}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">新密码</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="请输入新密码 (至少6位)"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">确认密码</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="请再次输入新密码"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPasswordModal(false)}
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSavePassword}
|
||||
disabled={isSaving}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
{isSaving ? "保存中..." : "确认修改"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] hover:bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">用户信息</TableHead>
|
||||
<TableHead className="text-gray-400">手机号</TableHead>
|
||||
<TableHead className="text-gray-400">购买状态</TableHead>
|
||||
<TableHead className="text-gray-400">分销收益</TableHead>
|
||||
<TableHead className="text-gray-400">注册时间</TableHead>
|
||||
<TableHead className="text-right text-gray-400">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<TableRow key={user.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
|
||||
{user.nickname.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-white">{user.nickname}</p>
|
||||
<p className="text-xs text-gray-500">ID: {user.id.slice(0, 8)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-300">{user.phone}</TableCell>
|
||||
<TableCell>
|
||||
{user.hasFullBook ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">全书已购</Badge>
|
||||
) : user.purchasedSections.length > 0 ? (
|
||||
<Badge className="bg-blue-500/20 text-blue-400 hover:bg-blue-500/20 border-0">
|
||||
已购 {user.purchasedSections.length} 节
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-gray-500 border-gray-600">
|
||||
未购买
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-white font-medium">¥{user.earnings?.toFixed(2) || "0.00"}</TableCell>
|
||||
<TableCell className="text-gray-400">{new Date(user.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="ghost" size="sm" className="text-gray-400 hover:text-white hover:bg-gray-700/50">
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={() => handleDelete(user.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
|
||||
<span className="ml-2 text-gray-400">加载中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] hover:bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">用户信息</TableHead>
|
||||
<TableHead className="text-gray-400">手机号</TableHead>
|
||||
<TableHead className="text-gray-400">购买状态</TableHead>
|
||||
<TableHead className="text-gray-400">分销收益</TableHead>
|
||||
<TableHead className="text-gray-400">今日匹配</TableHead>
|
||||
<TableHead className="text-gray-400">注册时间</TableHead>
|
||||
<TableHead className="text-right text-gray-400">操作</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredUsers.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-12 text-gray-500">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<TableRow key={user.id} className="hover:bg-[#0a1628] border-gray-700/50">
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
|
||||
{user.nickname?.charAt(0) || "?"}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-white">{user.nickname}</p>
|
||||
{user.is_admin && (
|
||||
<Badge className="bg-purple-500/20 text-purple-400 hover:bg-purple-500/20 border-0 text-xs">
|
||||
管理员
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">ID: {user.id?.slice(0, 8)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-300">{user.phone}</TableCell>
|
||||
<TableCell>
|
||||
{user.has_full_book ? (
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">全书已购</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-gray-500 border-gray-600">
|
||||
未购买
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-white font-medium">¥{(user.earnings || 0).toFixed(2)}</TableCell>
|
||||
<TableCell className="text-gray-300">{user.match_count_today || 0}/3</TableCell>
|
||||
<TableCell className="text-gray-400">
|
||||
{user.created_at ? new Date(user.created_at).toLocaleDateString() : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditUser(user)}
|
||||
className="text-gray-400 hover:text-[#38bdac] hover:bg-[#38bdac]/10"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleChangePassword(user)}
|
||||
className="text-gray-400 hover:text-yellow-400 hover:bg-yellow-400/10"
|
||||
>
|
||||
<Key className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
onClick={() => handleDelete(user.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredUsers.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center py-12 text-gray-500">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,38 @@ import { type NextRequest, NextResponse } from "next/server"
|
||||
import crypto from "crypto"
|
||||
|
||||
// 存客宝API配置
|
||||
const CKB_API_KEY = "fyngh-ecy9h-qkdae-epwd5-rz6kd"
|
||||
const CKB_API_KEY = process.env.CKB_API_KEY || "fyngh-ecy9h-qkdae-epwd5-rz6kd"
|
||||
const CKB_API_URL = "https://ckbapi.quwanzhi.com/v1/api/scenarios"
|
||||
|
||||
// 生成签名
|
||||
function generateSign(apiKey: string, timestamp: number): string {
|
||||
const signStr = `${apiKey}${timestamp}`
|
||||
return crypto.createHash("md5").update(signStr).digest("hex")
|
||||
// 生成签名 - 根据文档实现
|
||||
function generateSign(params: Record<string, any>, apiKey: string): string {
|
||||
// 1. 移除 sign、apiKey、portrait 字段
|
||||
const filteredParams = { ...params }
|
||||
delete filteredParams.sign
|
||||
delete filteredParams.apiKey
|
||||
delete filteredParams.portrait
|
||||
|
||||
// 2. 移除空值字段
|
||||
const nonEmptyParams: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(filteredParams)) {
|
||||
if (value !== null && value !== "") {
|
||||
nonEmptyParams[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 按参数名升序排序
|
||||
const sortedKeys = Object.keys(nonEmptyParams).sort()
|
||||
|
||||
// 4. 拼接参数值
|
||||
const stringToSign = sortedKeys.map(key => nonEmptyParams[key]).join("")
|
||||
|
||||
// 5. 第一次 MD5
|
||||
const firstMd5 = crypto.createHash("md5").update(stringToSign).digest("hex")
|
||||
|
||||
// 6. 拼接 apiKey 再次 MD5
|
||||
const sign = crypto.createHash("md5").update(firstMd5 + apiKey).digest("hex")
|
||||
|
||||
return sign
|
||||
}
|
||||
|
||||
// 不同类型对应的source标签
|
||||
@@ -16,44 +41,69 @@ const sourceMap: Record<string, string> = {
|
||||
team: "团队招募",
|
||||
investor: "资源对接",
|
||||
mentor: "导师顾问",
|
||||
partner: "创业合伙",
|
||||
}
|
||||
|
||||
const tagsMap: Record<string, string> = {
|
||||
team: "切片团队,团队招募",
|
||||
investor: "资源对接,资源群",
|
||||
mentor: "导师顾问,咨询服务",
|
||||
partner: "创业合伙,创业伙伴",
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { type, phone, name, wechatId, remark } = body
|
||||
const { type, phone, wechat, name, userId, remark } = body
|
||||
|
||||
// 验证必填参数
|
||||
if (!type || !phone) {
|
||||
return NextResponse.json({ success: false, message: "缺少必填参数" }, { status: 400 })
|
||||
// 验证必填参数 - 手机号或微信号至少一个
|
||||
if (!phone && !wechat) {
|
||||
return NextResponse.json({ success: false, message: "请提供手机号或微信号" }, { status: 400 })
|
||||
}
|
||||
|
||||
// 验证类型
|
||||
if (!["team", "investor", "mentor"].includes(type)) {
|
||||
if (!["team", "investor", "mentor", "partner"].includes(type)) {
|
||||
return NextResponse.json({ success: false, message: "无效的加入类型" }, { status: 400 })
|
||||
}
|
||||
|
||||
// 生成时间戳和签名
|
||||
// 生成时间戳(秒级)
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
const sign = generateSign(CKB_API_KEY, timestamp)
|
||||
|
||||
// 构建请求参数
|
||||
// 构建请求参数(不包含sign)
|
||||
const requestParams: Record<string, any> = {
|
||||
timestamp,
|
||||
source: `创业实验-${sourceMap[type]}`,
|
||||
tags: tagsMap[type],
|
||||
siteTags: "创业实验APP",
|
||||
remark: remark || `用户通过创业实验APP申请${sourceMap[type]}`,
|
||||
}
|
||||
|
||||
// 添加可选字段
|
||||
if (phone) requestParams.phone = phone
|
||||
if (wechat) requestParams.wechatId = wechat
|
||||
if (name) requestParams.name = name
|
||||
|
||||
// 生成签名
|
||||
const sign = generateSign(requestParams, CKB_API_KEY)
|
||||
|
||||
// 构建最终请求体
|
||||
const requestBody = {
|
||||
...requestParams,
|
||||
apiKey: CKB_API_KEY,
|
||||
sign,
|
||||
timestamp,
|
||||
phone,
|
||||
name: name || "",
|
||||
wechatId: wechatId || "",
|
||||
source: sourceMap[type],
|
||||
remark: remark || `来自创业实验APP-${sourceMap[type]}`,
|
||||
tags: tagsMap[type],
|
||||
portrait: {
|
||||
type: 4, // 互动行为
|
||||
source: 0, // 本站
|
||||
sourceData: {
|
||||
joinType: type,
|
||||
joinLabel: sourceMap[type],
|
||||
userId: userId || "",
|
||||
device: "webapp",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
remark: `${sourceMap[type]}申请`,
|
||||
uniqueId: `soul_${phone || wechat}_${timestamp}`,
|
||||
},
|
||||
}
|
||||
|
||||
// 调用存客宝API
|
||||
@@ -67,13 +117,14 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (response.ok && result.code === 0) {
|
||||
if (result.code === 200) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `成功加入${sourceMap[type]}`,
|
||||
message: result.message === "已存在" ? "您已加入,我们会尽快联系您" : `成功加入${sourceMap[type]}`,
|
||||
data: result.data,
|
||||
})
|
||||
} else {
|
||||
console.error("CKB API Error:", result)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: result.message || "加入失败,请稍后重试",
|
||||
|
||||
131
app/api/ckb/match/route.ts
Normal file
131
app/api/ckb/match/route.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import crypto from "crypto"
|
||||
|
||||
// 存客宝API配置
|
||||
const CKB_API_KEY = process.env.CKB_API_KEY || "fyngh-ecy9h-qkdae-epwd5-rz6kd"
|
||||
const CKB_API_URL = "https://ckbapi.quwanzhi.com/v1/api/scenarios"
|
||||
|
||||
// 生成签名 - 根据文档实现
|
||||
function generateSign(params: Record<string, any>, apiKey: string): string {
|
||||
// 1. 移除 sign、apiKey、portrait 字段
|
||||
const filteredParams = { ...params }
|
||||
delete filteredParams.sign
|
||||
delete filteredParams.apiKey
|
||||
delete filteredParams.portrait
|
||||
|
||||
// 2. 移除空值字段
|
||||
const nonEmptyParams: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(filteredParams)) {
|
||||
if (value !== null && value !== "") {
|
||||
nonEmptyParams[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 按参数名升序排序
|
||||
const sortedKeys = Object.keys(nonEmptyParams).sort()
|
||||
|
||||
// 4. 拼接参数值
|
||||
const stringToSign = sortedKeys.map(key => nonEmptyParams[key]).join("")
|
||||
|
||||
// 5. 第一次 MD5
|
||||
const firstMd5 = crypto.createHash("md5").update(stringToSign).digest("hex")
|
||||
|
||||
// 6. 拼接 apiKey 再次 MD5
|
||||
const sign = crypto.createHash("md5").update(firstMd5 + apiKey).digest("hex")
|
||||
|
||||
return sign
|
||||
}
|
||||
|
||||
// 匹配类型映射
|
||||
const matchTypeMap: Record<string, string> = {
|
||||
partner: "创业合伙",
|
||||
investor: "资源对接",
|
||||
mentor: "导师顾问",
|
||||
team: "团队招募",
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { matchType, phone, wechat, userId, nickname, matchedUser } = body
|
||||
|
||||
// 验证必填参数 - 手机号或微信号至少一个
|
||||
if (!phone && !wechat) {
|
||||
return NextResponse.json({ success: false, message: "请提供手机号或微信号" }, { status: 400 })
|
||||
}
|
||||
|
||||
// 生成时间戳(秒级)
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
|
||||
// 构建请求参数(不包含sign)
|
||||
const requestParams: Record<string, any> = {
|
||||
timestamp,
|
||||
source: `创业实验-找伙伴匹配`,
|
||||
tags: `找伙伴,${matchTypeMap[matchType] || "创业合伙"}`,
|
||||
siteTags: "创业实验APP,匹配用户",
|
||||
remark: `用户发起${matchTypeMap[matchType] || "创业合伙"}匹配`,
|
||||
}
|
||||
|
||||
// 添加联系方式
|
||||
if (phone) requestParams.phone = phone
|
||||
if (wechat) requestParams.wechatId = wechat
|
||||
if (nickname) requestParams.name = nickname
|
||||
|
||||
// 生成签名
|
||||
const sign = generateSign(requestParams, CKB_API_KEY)
|
||||
|
||||
// 构建最终请求体
|
||||
const requestBody = {
|
||||
...requestParams,
|
||||
apiKey: CKB_API_KEY,
|
||||
sign,
|
||||
portrait: {
|
||||
type: 4, // 互动行为
|
||||
source: 0, // 本站
|
||||
sourceData: {
|
||||
action: "match",
|
||||
matchType: matchType,
|
||||
matchLabel: matchTypeMap[matchType] || "创业合伙",
|
||||
userId: userId || "",
|
||||
matchedUserId: matchedUser?.id || "",
|
||||
matchedUserNickname: matchedUser?.nickname || "",
|
||||
matchScore: matchedUser?.matchScore || 0,
|
||||
device: "webapp",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
remark: `找伙伴匹配-${matchTypeMap[matchType] || "创业合伙"}`,
|
||||
uniqueId: `soul_match_${phone || wechat}_${timestamp}`,
|
||||
},
|
||||
}
|
||||
|
||||
// 调用存客宝API
|
||||
const response = await fetch(CKB_API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code === 200) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "匹配记录已上报",
|
||||
data: result.data,
|
||||
})
|
||||
} else {
|
||||
console.error("CKB Match API Error:", result)
|
||||
// 即使上报失败也返回成功,不影响用户体验
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "匹配成功",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("存客宝匹配API调用失败:", error)
|
||||
// 即使出错也返回成功,不影响用户体验
|
||||
return NextResponse.json({ success: true, message: "匹配成功" })
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,33 @@ export async function GET() {
|
||||
enabled: true,
|
||||
qrCode: "/images/wechat-pay.png",
|
||||
account: "卡若",
|
||||
appId: process.env.TENCENT_APP_ID || "1251077262", // From .env or default
|
||||
// 敏感信息后端处理,不完全暴露给前端
|
||||
websiteAppId: "wx432c93e275548671",
|
||||
websiteAppSecret: "25b7e7fdb7998e5107e242ebb6ddabd0",
|
||||
serviceAppId: "wx7c0dbf34ddba300d",
|
||||
serviceAppSecret: "f865ef18c43dfea6cbe3b1f1aebdb82e",
|
||||
mpVerifyCode: "SP8AfZJyAvprRORT",
|
||||
merchantId: "1318592501",
|
||||
apiKey: "wx3e31b068be59ddc131b068be59ddc2",
|
||||
groupQrCode: "/images/party-group-qr.png",
|
||||
},
|
||||
alipay: {
|
||||
enabled: true,
|
||||
qrCode: "/images/alipay.png",
|
||||
account: "卡若",
|
||||
appId: process.env.ALIPAY_ACCESS_KEY_ID || "LTAI5t9zkiWmFtHG8qmtdysW", // Using Access Key as placeholder ID
|
||||
partnerId: "2088511801157159",
|
||||
securityKey: "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
|
||||
mobilePayEnabled: true,
|
||||
paymentInterface: "official_instant",
|
||||
},
|
||||
usdt: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
network: "TRC20",
|
||||
address: process.env.USDT_WALLET_ADDRESS || "TWeq9xxxxxxxxxxxxxxxxxxxx",
|
||||
address: "",
|
||||
exchangeRate: 7.2
|
||||
},
|
||||
paypal: {
|
||||
enabled: false,
|
||||
email: process.env.PAYPAL_CLIENT_ID || "",
|
||||
email: "",
|
||||
exchangeRate: 7.2
|
||||
}
|
||||
},
|
||||
@@ -41,9 +50,15 @@ export async function GET() {
|
||||
},
|
||||
authorInfo: {
|
||||
name: "卡若",
|
||||
description: "私域运营与技术公司主理人",
|
||||
description: "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
|
||||
liveTime: "06:00-09:00",
|
||||
platform: "Soul"
|
||||
platform: "Soul派对房"
|
||||
},
|
||||
siteConfig: {
|
||||
siteName: "一场soul的创业实验",
|
||||
siteTitle: "一场soul的创业实验",
|
||||
siteDescription: "来自Soul派对房的真实商业故事",
|
||||
primaryColor: "#00CED1"
|
||||
},
|
||||
system: {
|
||||
version: "1.0.0",
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function GET(request: NextRequest) {
|
||||
const content = fs.readFileSync(fullPath, "utf-8")
|
||||
return NextResponse.json({ content, isCustom: false })
|
||||
} catch (error) {
|
||||
console.error("[v0] Error reading file:", error)
|
||||
console.error("[Karuo] Error reading file:", error)
|
||||
return NextResponse.json({ error: "Failed to read file" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
194
app/api/db/book/route.ts
Normal file
194
app/api/db/book/route.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { bookDB } from '@/lib/db'
|
||||
import { bookData } from '@/lib/book-data'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
// 获取章节
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const id = searchParams.get('id')
|
||||
const action = searchParams.get('action')
|
||||
|
||||
// 导出所有章节
|
||||
if (action === 'export') {
|
||||
const data = await bookDB.exportAll()
|
||||
return new NextResponse(data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': 'attachment; filename=book_sections.json'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 从文件系统读取章节内容
|
||||
if (action === 'read' && id) {
|
||||
// 查找章节文件路径
|
||||
let filePath = ''
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find(s => s.id === id)
|
||||
if (section) {
|
||||
filePath = section.filePath
|
||||
break
|
||||
}
|
||||
}
|
||||
if (filePath) break
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '章节不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
const fullPath = path.join(process.cwd(), filePath)
|
||||
const content = await fs.readFile(fullPath, 'utf-8')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
section: { id, filePath, content }
|
||||
})
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const section = await bookDB.getSection(id)
|
||||
return NextResponse.json({ success: true, section })
|
||||
}
|
||||
|
||||
const sections = await bookDB.getAllSections()
|
||||
return NextResponse.json({ success: true, sections })
|
||||
} catch (error: any) {
|
||||
console.error('Get book sections error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 创建或更新章节
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { action, data } = body
|
||||
|
||||
// 导入章节
|
||||
if (action === 'import') {
|
||||
const count = await bookDB.importSections(JSON.stringify(data))
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `成功导入 ${count} 个章节`
|
||||
})
|
||||
}
|
||||
|
||||
// 同步book-data到数据库
|
||||
if (action === 'sync') {
|
||||
let count = 0
|
||||
let sortOrder = 0
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
sortOrder++
|
||||
const existing = await bookDB.getSection(section.id)
|
||||
|
||||
// 读取文件内容
|
||||
let content = ''
|
||||
try {
|
||||
const fullPath = path.join(process.cwd(), section.filePath)
|
||||
content = await fs.readFile(fullPath, 'utf-8')
|
||||
} catch (e) {
|
||||
console.warn(`Cannot read file: ${section.filePath}`)
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
await bookDB.updateSection(section.id, {
|
||||
title: section.title,
|
||||
content,
|
||||
price: section.price,
|
||||
is_free: section.isFree
|
||||
})
|
||||
} else {
|
||||
await bookDB.createSection({
|
||||
id: section.id,
|
||||
part_id: part.id,
|
||||
chapter_id: chapter.id,
|
||||
title: section.title,
|
||||
content,
|
||||
price: section.price,
|
||||
is_free: section.isFree,
|
||||
sort_order: sortOrder
|
||||
})
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `成功同步 ${count} 个章节到数据库`
|
||||
})
|
||||
}
|
||||
|
||||
// 创建单个章节
|
||||
const section = await bookDB.createSection(data)
|
||||
return NextResponse.json({ success: true, section })
|
||||
} catch (error: any) {
|
||||
console.error('Create/Import book section error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 更新章节
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { id, ...updates } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少章节ID'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 如果要保存到文件系统
|
||||
if (updates.content && updates.saveToFile) {
|
||||
// 查找章节文件路径
|
||||
let filePath = ''
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find(s => s.id === id)
|
||||
if (section) {
|
||||
filePath = section.filePath
|
||||
break
|
||||
}
|
||||
}
|
||||
if (filePath) break
|
||||
}
|
||||
|
||||
if (filePath) {
|
||||
const fullPath = path.join(process.cwd(), filePath)
|
||||
await fs.writeFile(fullPath, updates.content, 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
await bookDB.updateSection(id, updates)
|
||||
const section = await bookDB.getSection(id)
|
||||
|
||||
return NextResponse.json({ success: true, section })
|
||||
} catch (error: any) {
|
||||
console.error('Update book section error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
272
app/api/db/chapters/route.ts
Normal file
272
app/api/db/chapters/route.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Soul创业实验 - 文章数据API
|
||||
* 用于存储和获取章节数据
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
// 文章数据结构
|
||||
interface Section {
|
||||
id: string
|
||||
title: string
|
||||
isFree: boolean
|
||||
price: number
|
||||
content?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
interface Chapter {
|
||||
id: string
|
||||
title: string
|
||||
sections: Section[]
|
||||
}
|
||||
|
||||
interface Part {
|
||||
id: string
|
||||
number: string
|
||||
title: string
|
||||
subtitle: string
|
||||
chapters: Chapter[]
|
||||
}
|
||||
|
||||
// 书籍目录结构映射
|
||||
const BOOK_STRUCTURE = [
|
||||
{
|
||||
id: 'part-1',
|
||||
number: '一',
|
||||
title: '真实的人',
|
||||
subtitle: '人与人之间的底层逻辑',
|
||||
folder: '第一篇|真实的人',
|
||||
chapters: [
|
||||
{ id: 'chapter-1', title: '第1章|人与人之间的底层逻辑', folder: '第1章|人与人之间的底层逻辑' },
|
||||
{ id: 'chapter-2', title: '第2章|人性困境案例', folder: '第2章|人性困境案例' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-2',
|
||||
number: '二',
|
||||
title: '真实的行业',
|
||||
subtitle: '电商、内容、传统行业解析',
|
||||
folder: '第二篇|真实的行业',
|
||||
chapters: [
|
||||
{ id: 'chapter-3', title: '第3章|电商篇', folder: '第3章|电商篇' },
|
||||
{ id: 'chapter-4', title: '第4章|内容商业篇', folder: '第4章|内容商业篇' },
|
||||
{ id: 'chapter-5', title: '第5章|传统行业篇', folder: '第5章|传统行业篇' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-3',
|
||||
number: '三',
|
||||
title: '真实的错误',
|
||||
subtitle: '我和别人犯过的错',
|
||||
folder: '第三篇|真实的错误',
|
||||
chapters: [
|
||||
{ id: 'chapter-6', title: '第6章|我人生错过的4件大钱', folder: '第6章|我人生错过的4件大钱' },
|
||||
{ id: 'chapter-7', title: '第7章|别人犯的错误', folder: '第7章|别人犯的错误' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-4',
|
||||
number: '四',
|
||||
title: '真实的赚钱',
|
||||
subtitle: '底层结构与真实案例',
|
||||
folder: '第四篇|真实的赚钱',
|
||||
chapters: [
|
||||
{ id: 'chapter-8', title: '第8章|底层结构', folder: '第8章|底层结构' },
|
||||
{ id: 'chapter-9', title: '第9章|我在Soul上亲访的赚钱案例', folder: '第9章|我在Soul上亲访的赚钱案例' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-5',
|
||||
number: '五',
|
||||
title: '真实的社会',
|
||||
subtitle: '未来职业与商业生态',
|
||||
folder: '第五篇|真实的社会',
|
||||
chapters: [
|
||||
{ id: 'chapter-10', title: '第10章|未来职业的变化趋势', folder: '第10章|未来职业的变化趋势' },
|
||||
{ id: 'chapter-11', title: '第11章|中国社会商业生态的未来', folder: '第11章|中国社会商业生态的未来' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 免费章节ID
|
||||
const FREE_SECTIONS = ['1.1', 'preface', 'epilogue', 'appendix-1', 'appendix-2', 'appendix-3']
|
||||
|
||||
// 从book目录读取真实文章数据
|
||||
function loadBookData(): Part[] {
|
||||
const bookPath = path.join(process.cwd(), 'book')
|
||||
const parts: Part[] = []
|
||||
|
||||
for (const partConfig of BOOK_STRUCTURE) {
|
||||
const part: Part = {
|
||||
id: partConfig.id,
|
||||
number: partConfig.number,
|
||||
title: partConfig.title,
|
||||
subtitle: partConfig.subtitle,
|
||||
chapters: []
|
||||
}
|
||||
|
||||
for (const chapterConfig of partConfig.chapters) {
|
||||
const chapter: Chapter = {
|
||||
id: chapterConfig.id,
|
||||
title: chapterConfig.title,
|
||||
sections: []
|
||||
}
|
||||
|
||||
const chapterPath = path.join(bookPath, partConfig.folder, chapterConfig.folder)
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(chapterPath)
|
||||
const mdFiles = files.filter(f => f.endsWith('.md')).sort()
|
||||
|
||||
for (const file of mdFiles) {
|
||||
// 从文件名提取ID和标题
|
||||
const match = file.match(/^(\d+\.\d+)\s+(.+)\.md$/)
|
||||
if (match) {
|
||||
const [, id, title] = match
|
||||
const filePath = path.join(chapterPath, file)
|
||||
|
||||
chapter.sections.push({
|
||||
id,
|
||||
title: title.replace(/[::]/g, ':'), // 统一冒号格式
|
||||
isFree: FREE_SECTIONS.includes(id),
|
||||
price: 1,
|
||||
filePath
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按ID数字排序
|
||||
chapter.sections.sort((a, b) => {
|
||||
const [aMajor, aMinor] = a.id.split('.').map(Number)
|
||||
const [bMajor, bMinor] = b.id.split('.').map(Number)
|
||||
return aMajor !== bMajor ? aMajor - bMajor : aMinor - bMinor
|
||||
})
|
||||
|
||||
} catch (e) {
|
||||
console.error(`读取章节目录失败: ${chapterPath}`, e)
|
||||
}
|
||||
|
||||
part.chapters.push(chapter)
|
||||
}
|
||||
|
||||
parts.push(part)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
// 读取文章内容
|
||||
function getArticleContent(sectionId: string): string | null {
|
||||
const bookData = loadBookData()
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find(s => s.id === sectionId)
|
||||
if (section?.filePath) {
|
||||
try {
|
||||
return fs.readFileSync(section.filePath, 'utf-8')
|
||||
} catch (e) {
|
||||
console.error(`读取文章内容失败: ${section.filePath}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// GET - 获取所有章节数据
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const sectionId = searchParams.get('id')
|
||||
const includeContent = searchParams.get('content') === 'true'
|
||||
|
||||
try {
|
||||
// 如果指定了章节ID,返回单篇文章内容
|
||||
if (sectionId) {
|
||||
const content = getArticleContent(sectionId)
|
||||
if (content) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { id: sectionId, content }
|
||||
})
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '文章不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
// 返回完整书籍结构
|
||||
const bookData = loadBookData()
|
||||
|
||||
// 统计总章节数
|
||||
let totalSections = 0
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
totalSections += chapter.sections.length
|
||||
}
|
||||
}
|
||||
// 加上序言、尾声和3个附录
|
||||
totalSections += 5
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalSections,
|
||||
parts: bookData,
|
||||
appendix: [
|
||||
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话', isFree: true },
|
||||
{ id: 'appendix-2', title: '附录2|创业者自检清单', isFree: true },
|
||||
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源', isFree: true }
|
||||
],
|
||||
preface: { id: 'preface', title: '序言|为什么我每天早上6点在Soul开播?', isFree: true },
|
||||
epilogue: { id: 'epilogue', title: '尾声|这本书的真实目的', isFree: true }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取章节数据失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '获取数据失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST - 同步章节数据到数据库(预留接口)
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const bookData = loadBookData()
|
||||
|
||||
// 这里可以添加数据库写入逻辑
|
||||
// 目前先返回成功,数据已从文件系统读取
|
||||
|
||||
let totalSections = 0
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
totalSections += chapter.sections.length
|
||||
}
|
||||
}
|
||||
totalSections += 5 // 序言、尾声、3个附录
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '章节数据同步成功',
|
||||
data: {
|
||||
totalSections,
|
||||
partsCount: bookData.length,
|
||||
chaptersCount: bookData.reduce((acc, p) => acc + p.chapters.length, 0)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('同步章节数据失败:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '同步数据失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
96
app/api/db/distribution/route.ts
Normal file
96
app/api/db/distribution/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { distributionDB, purchaseDB } from '@/lib/db'
|
||||
|
||||
// 获取分销数据
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const type = searchParams.get('type')
|
||||
const referrerId = searchParams.get('referrer_id')
|
||||
|
||||
// 获取佣金记录
|
||||
if (type === 'commissions') {
|
||||
const commissions = await distributionDB.getAllCommissions()
|
||||
return NextResponse.json({ success: true, commissions })
|
||||
}
|
||||
|
||||
// 获取指定推荐人的绑定
|
||||
if (referrerId) {
|
||||
const bindings = await distributionDB.getBindingsByReferrer(referrerId)
|
||||
return NextResponse.json({ success: true, bindings })
|
||||
}
|
||||
|
||||
// 获取所有绑定关系
|
||||
const bindings = await distributionDB.getAllBindings()
|
||||
return NextResponse.json({ success: true, bindings })
|
||||
} catch (error: any) {
|
||||
console.error('Get distribution data error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 创建绑定或佣金记录
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { type, data } = body
|
||||
|
||||
if (type === 'binding') {
|
||||
const binding = await distributionDB.createBinding({
|
||||
id: `binding_${Date.now()}`,
|
||||
...data,
|
||||
bound_at: new Date().toISOString(),
|
||||
expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30天有效期
|
||||
status: 'active'
|
||||
})
|
||||
return NextResponse.json({ success: true, binding })
|
||||
}
|
||||
|
||||
if (type === 'commission') {
|
||||
const commission = await distributionDB.createCommission({
|
||||
id: `commission_${Date.now()}`,
|
||||
...data,
|
||||
status: 'pending'
|
||||
})
|
||||
return NextResponse.json({ success: true, commission })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '未知操作类型'
|
||||
}, { status: 400 })
|
||||
} catch (error: any) {
|
||||
console.error('Create distribution record error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 更新绑定状态
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { id, status } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少记录ID'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
await distributionDB.updateBindingStatus(id, status)
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
console.error('Update distribution status error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
16
app/api/db/init/route.ts
Normal file
16
app/api/db/init/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { initDatabase } from '@/lib/db'
|
||||
|
||||
// 初始化数据库表
|
||||
export async function POST() {
|
||||
try {
|
||||
await initDatabase()
|
||||
return NextResponse.json({ success: true, message: '数据库初始化成功' })
|
||||
} catch (error: any) {
|
||||
console.error('Database init error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message || '数据库初始化失败'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
106
app/api/db/purchases/route.ts
Normal file
106
app/api/db/purchases/route.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { purchaseDB, userDB, distributionDB } from '@/lib/db'
|
||||
|
||||
// 获取购买记录
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const userId = searchParams.get('user_id')
|
||||
|
||||
if (userId) {
|
||||
const purchases = await purchaseDB.getByUserId(userId)
|
||||
return NextResponse.json({ success: true, purchases })
|
||||
}
|
||||
|
||||
const purchases = await purchaseDB.getAll()
|
||||
return NextResponse.json({ success: true, purchases })
|
||||
} catch (error: any) {
|
||||
console.error('Get purchases error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 创建购买记录
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const {
|
||||
user_id,
|
||||
type,
|
||||
section_id,
|
||||
section_title,
|
||||
amount,
|
||||
payment_method,
|
||||
referral_code
|
||||
} = body
|
||||
|
||||
// 创建购买记录
|
||||
const purchase = await purchaseDB.create({
|
||||
id: `purchase_${Date.now()}`,
|
||||
user_id,
|
||||
type,
|
||||
section_id,
|
||||
section_title,
|
||||
amount,
|
||||
payment_method,
|
||||
referral_code,
|
||||
referrer_earnings: 0,
|
||||
status: 'completed'
|
||||
})
|
||||
|
||||
// 更新用户购买状态
|
||||
if (type === 'fullbook') {
|
||||
await userDB.update(user_id, { has_full_book: true })
|
||||
}
|
||||
|
||||
// 处理分销佣金
|
||||
if (referral_code) {
|
||||
// 查找推荐人
|
||||
const users = await userDB.getAll()
|
||||
const referrer = users.find((u: any) => u.referral_code === referral_code)
|
||||
|
||||
if (referrer) {
|
||||
const commissionRate = 0.9 // 90% 佣金
|
||||
const commissionAmount = amount * commissionRate
|
||||
|
||||
// 查找有效的绑定关系
|
||||
const binding = await distributionDB.getActiveBindingByReferee(user_id)
|
||||
|
||||
if (binding) {
|
||||
// 创建佣金记录
|
||||
await distributionDB.createCommission({
|
||||
id: `commission_${Date.now()}`,
|
||||
binding_id: binding.id,
|
||||
referrer_id: referrer.id,
|
||||
referee_id: user_id,
|
||||
order_id: purchase.id,
|
||||
amount,
|
||||
commission_rate: commissionRate * 100,
|
||||
commission_amount: commissionAmount,
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
// 更新推荐人收益
|
||||
await userDB.update(referrer.id, {
|
||||
earnings: (referrer.earnings || 0) + commissionAmount,
|
||||
pending_earnings: (referrer.pending_earnings || 0) + commissionAmount
|
||||
})
|
||||
|
||||
// 更新购买记录的推荐人收益
|
||||
purchase.referrer_earnings = commissionAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, purchase })
|
||||
} catch (error: any) {
|
||||
console.error('Create purchase error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
31
app/api/db/settings/route.ts
Normal file
31
app/api/db/settings/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { settingsDB } from '@/lib/db'
|
||||
|
||||
// 获取系统设置
|
||||
export async function GET() {
|
||||
try {
|
||||
const settings = await settingsDB.get()
|
||||
return NextResponse.json({ success: true, settings: settings?.data || null })
|
||||
} catch (error: any) {
|
||||
console.error('Get settings error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 保存系统设置
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
await settingsDB.update(body)
|
||||
return NextResponse.json({ success: true, message: '设置已保存' })
|
||||
} catch (error: any) {
|
||||
console.error('Save settings error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
114
app/api/db/users/route.ts
Normal file
114
app/api/db/users/route.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { userDB } from '@/lib/db'
|
||||
|
||||
// 获取所有用户
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const id = searchParams.get('id')
|
||||
const phone = searchParams.get('phone')
|
||||
|
||||
if (id) {
|
||||
const user = await userDB.getById(id)
|
||||
return NextResponse.json({ success: true, user })
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
const user = await userDB.getByPhone(phone)
|
||||
return NextResponse.json({ success: true, user })
|
||||
}
|
||||
|
||||
const users = await userDB.getAll()
|
||||
return NextResponse.json({ success: true, users })
|
||||
} catch (error: any) {
|
||||
console.error('Get users error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { phone, nickname, password, referral_code, referred_by } = body
|
||||
|
||||
// 检查手机号是否已存在
|
||||
const existing = await userDB.getByPhone(phone)
|
||||
if (existing) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '该手机号已注册'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const user = await userDB.create({
|
||||
id: `user_${Date.now()}`,
|
||||
phone,
|
||||
nickname,
|
||||
password,
|
||||
referral_code: referral_code || `REF${Date.now().toString(36).toUpperCase()}`,
|
||||
referred_by
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, user })
|
||||
} catch (error: any) {
|
||||
console.error('Create user error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { id, ...updates } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少用户ID'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
await userDB.update(id, updates)
|
||||
const user = await userDB.getById(id)
|
||||
|
||||
return NextResponse.json({ success: true, user })
|
||||
} catch (error: any) {
|
||||
console.error('Update user error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const id = searchParams.get('id')
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少用户ID'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
await userDB.delete(id)
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
console.error('Delete user error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
113
app/api/db/withdrawals/route.ts
Normal file
113
app/api/db/withdrawals/route.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { withdrawalDB, userDB } from '@/lib/db'
|
||||
|
||||
// 获取提现记录
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const userId = searchParams.get('user_id')
|
||||
|
||||
if (userId) {
|
||||
const withdrawals = await withdrawalDB.getByUserId(userId)
|
||||
return NextResponse.json({ success: true, withdrawals })
|
||||
}
|
||||
|
||||
const withdrawals = await withdrawalDB.getAll()
|
||||
return NextResponse.json({ success: true, withdrawals })
|
||||
} catch (error: any) {
|
||||
console.error('Get withdrawals error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 创建提现申请
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { user_id, amount, method, account, name } = body
|
||||
|
||||
// 验证用户余额
|
||||
const user = await userDB.getById(user_id)
|
||||
if (!user) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
if ((user.earnings || 0) < amount) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '余额不足'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// 创建提现记录
|
||||
const withdrawal = await withdrawalDB.create({
|
||||
id: `withdrawal_${Date.now()}`,
|
||||
user_id,
|
||||
amount,
|
||||
method,
|
||||
account,
|
||||
name,
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
// 扣除用户余额,增加待提现金额
|
||||
await userDB.update(user_id, {
|
||||
earnings: (user.earnings || 0) - amount,
|
||||
pending_earnings: (user.pending_earnings || 0) + amount
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, withdrawal })
|
||||
} catch (error: any) {
|
||||
console.error('Create withdrawal error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 更新提现状态
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { id, status } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '缺少提现记录ID'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
await withdrawalDB.updateStatus(id, status)
|
||||
|
||||
// 如果状态是已完成,更新用户的已提现金额
|
||||
if (status === 'completed') {
|
||||
const withdrawals = await withdrawalDB.getAll()
|
||||
const withdrawal = withdrawals.find((w: any) => w.id === id)
|
||||
if (withdrawal) {
|
||||
const user = await userDB.getById(withdrawal.user_id)
|
||||
if (user) {
|
||||
await userDB.update(user.id, {
|
||||
pending_earnings: (user.pending_earnings || 0) - withdrawal.amount,
|
||||
withdrawn_earnings: (user.withdrawn_earnings || 0) + withdrawal.amount
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
console.error('Update withdrawal status error:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: error.message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
108
app/api/distribution/auto-withdraw-config/route.ts
Normal file
108
app/api/distribution/auto-withdraw-config/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 自动提现配置API
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
// 内存存储(实际应用中应该存入数据库)
|
||||
const autoWithdrawConfigs: Map<string, {
|
||||
userId: string;
|
||||
enabled: boolean;
|
||||
minAmount: number;
|
||||
method: 'wechat' | 'alipay';
|
||||
account: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}> = new Map();
|
||||
|
||||
// GET: 获取用户自动提现配置
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const userId = searchParams.get('userId');
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: '缺少用户ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const config = autoWithdrawConfigs.get(userId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config: config || null,
|
||||
});
|
||||
}
|
||||
|
||||
// POST: 保存/更新自动提现配置
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { userId, enabled, minAmount, method, account, name } = body;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: '缺少用户ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if (enabled) {
|
||||
if (!minAmount || minAmount < 10) {
|
||||
return NextResponse.json({ error: '最低提现金额不能少于10元' }, { status: 400 });
|
||||
}
|
||||
if (!account) {
|
||||
return NextResponse.json({ error: '请填写提现账号' }, { status: 400 });
|
||||
}
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: '请填写真实姓名' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const existingConfig = autoWithdrawConfigs.get(userId);
|
||||
|
||||
const config = {
|
||||
userId,
|
||||
enabled: Boolean(enabled),
|
||||
minAmount: Number(minAmount) || 100,
|
||||
method: method || 'wechat',
|
||||
account: account || '',
|
||||
name: name || '',
|
||||
createdAt: existingConfig?.createdAt || now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
autoWithdrawConfigs.set(userId, config);
|
||||
|
||||
console.log('[AutoWithdrawConfig] 保存配置:', {
|
||||
userId,
|
||||
enabled: config.enabled,
|
||||
minAmount: config.minAmount,
|
||||
method: config.method,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config,
|
||||
message: enabled ? '自动提现已启用' : '自动提现已关闭',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[AutoWithdrawConfig] 保存失败:', error);
|
||||
return NextResponse.json({ error: '保存配置失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE: 删除自动提现配置
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const userId = searchParams.get('userId');
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: '缺少用户ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
autoWithdrawConfigs.delete(userId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '配置已删除',
|
||||
});
|
||||
}
|
||||
53
app/api/distribution/messages/route.ts
Normal file
53
app/api/distribution/messages/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 分销消息API
|
||||
* 用于WebSocket轮询获取实时消息
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getMessages, clearMessages } from '@/lib/modules/distribution/websocket';
|
||||
|
||||
// GET: 获取用户消息
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const userId = searchParams.get('userId');
|
||||
const since = searchParams.get('since');
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: '缺少用户ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const messages = getMessages(userId, since || undefined);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
messages,
|
||||
count: messages.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[MessagesAPI] 获取消息失败:', error);
|
||||
return NextResponse.json({ error: '获取消息失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST: 标记消息已读
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { userId, messageIds } = body;
|
||||
|
||||
if (!userId || !messageIds || !Array.isArray(messageIds)) {
|
||||
return NextResponse.json({ error: '参数错误' }, { status: 400 });
|
||||
}
|
||||
|
||||
clearMessages(userId, messageIds);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '消息已标记为已读',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[MessagesAPI] 标记已读失败:', error);
|
||||
return NextResponse.json({ error: '操作失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
885
app/api/distribution/route.ts
Normal file
885
app/api/distribution/route.ts
Normal file
@@ -0,0 +1,885 @@
|
||||
/**
|
||||
* 分销模块API
|
||||
* 功能:绑定追踪、提现管理、统计概览
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
// 模拟数据存储(实际项目应使用数据库)
|
||||
let distributionBindings: Array<{
|
||||
id: string;
|
||||
referrerId: string;
|
||||
referrerCode: string;
|
||||
visitorId: string;
|
||||
visitorPhone?: string;
|
||||
visitorNickname?: string;
|
||||
bindingTime: string;
|
||||
expireTime: string;
|
||||
status: 'active' | 'converted' | 'expired' | 'cancelled';
|
||||
convertedAt?: string;
|
||||
orderId?: string;
|
||||
orderAmount?: number;
|
||||
commission?: number;
|
||||
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
|
||||
createdAt: string;
|
||||
}> = [];
|
||||
|
||||
let clickRecords: Array<{
|
||||
id: string;
|
||||
referralCode: string;
|
||||
referrerId: string;
|
||||
visitorId: string;
|
||||
source: string;
|
||||
clickTime: string;
|
||||
}> = [];
|
||||
|
||||
let distributors: Array<{
|
||||
id: string;
|
||||
userId: string;
|
||||
nickname: string;
|
||||
phone: string;
|
||||
referralCode: string;
|
||||
totalClicks: number;
|
||||
totalBindings: number;
|
||||
activeBindings: number;
|
||||
convertedBindings: number;
|
||||
expiredBindings: number;
|
||||
totalEarnings: number;
|
||||
pendingEarnings: number;
|
||||
withdrawnEarnings: number;
|
||||
autoWithdraw: boolean;
|
||||
autoWithdrawThreshold: number;
|
||||
autoWithdrawAccount?: {
|
||||
type: 'wechat' | 'alipay';
|
||||
account: string;
|
||||
name: string;
|
||||
};
|
||||
level: 'normal' | 'silver' | 'gold' | 'diamond';
|
||||
commissionRate: number;
|
||||
status: 'active' | 'frozen' | 'disabled';
|
||||
createdAt: string;
|
||||
}> = [];
|
||||
|
||||
let withdrawRecords: Array<{
|
||||
id: string;
|
||||
distributorId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
amount: number;
|
||||
fee: number;
|
||||
actualAmount: number;
|
||||
method: 'wechat' | 'alipay';
|
||||
account: string;
|
||||
accountName: string;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed' | 'rejected';
|
||||
isAuto: boolean;
|
||||
paymentNo?: string;
|
||||
paymentTime?: string;
|
||||
reviewNote?: string;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
}> = [];
|
||||
|
||||
// 配置
|
||||
const BINDING_DAYS = 30;
|
||||
const MIN_WITHDRAW_AMOUNT = 10;
|
||||
const DEFAULT_COMMISSION_RATE = 90;
|
||||
|
||||
// 生成ID
|
||||
function generateId(prefix: string = ''): string {
|
||||
return `${prefix}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
// GET: 获取分销数据
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const type = searchParams.get('type') || 'overview';
|
||||
const userId = searchParams.get('userId');
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || '20');
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case 'overview':
|
||||
return getOverview();
|
||||
|
||||
case 'bindings':
|
||||
return getBindings(userId, page, pageSize);
|
||||
|
||||
case 'my-bindings':
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: '缺少用户ID' }, { status: 400 });
|
||||
}
|
||||
return getMyBindings(userId);
|
||||
|
||||
case 'withdrawals':
|
||||
return getWithdrawals(userId, page, pageSize);
|
||||
|
||||
case 'reminders':
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: '缺少用户ID' }, { status: 400 });
|
||||
}
|
||||
return getReminders(userId);
|
||||
|
||||
case 'distributor':
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: '缺少用户ID' }, { status: 400 });
|
||||
}
|
||||
return getDistributor(userId);
|
||||
|
||||
case 'ranking':
|
||||
return getRanking();
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: '未知类型' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('分销API错误:', error);
|
||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST: 分销操作
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { action } = body;
|
||||
|
||||
switch (action) {
|
||||
case 'record_click':
|
||||
return recordClick(body);
|
||||
|
||||
case 'convert':
|
||||
return convertBinding(body);
|
||||
|
||||
case 'request_withdraw':
|
||||
return requestWithdraw(body);
|
||||
|
||||
case 'set_auto_withdraw':
|
||||
return setAutoWithdraw(body);
|
||||
|
||||
case 'process_expired':
|
||||
return processExpiredBindings();
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('分销API错误:', error);
|
||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// PUT: 更新操作(后台管理)
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { action } = body;
|
||||
|
||||
switch (action) {
|
||||
case 'approve_withdraw':
|
||||
return approveWithdraw(body);
|
||||
|
||||
case 'reject_withdraw':
|
||||
return rejectWithdraw(body);
|
||||
|
||||
case 'update_distributor':
|
||||
return updateDistributor(body);
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('分销API错误:', error);
|
||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 具体实现 ==========
|
||||
|
||||
// 获取概览数据
|
||||
function getOverview() {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const weekFromNow = new Date();
|
||||
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
||||
|
||||
const overview = {
|
||||
// 今日数据
|
||||
todayClicks: clickRecords.filter(c => new Date(c.clickTime) >= today).length,
|
||||
todayBindings: distributionBindings.filter(b => new Date(b.createdAt) >= today).length,
|
||||
todayConversions: distributionBindings.filter(b =>
|
||||
b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= today
|
||||
).length,
|
||||
todayEarnings: distributionBindings
|
||||
.filter(b => b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= today)
|
||||
.reduce((sum, b) => sum + (b.commission || 0), 0),
|
||||
|
||||
// 本月数据
|
||||
monthClicks: clickRecords.filter(c => new Date(c.clickTime) >= monthStart).length,
|
||||
monthBindings: distributionBindings.filter(b => new Date(b.createdAt) >= monthStart).length,
|
||||
monthConversions: distributionBindings.filter(b =>
|
||||
b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= monthStart
|
||||
).length,
|
||||
monthEarnings: distributionBindings
|
||||
.filter(b => b.status === 'converted' && b.convertedAt && new Date(b.convertedAt) >= monthStart)
|
||||
.reduce((sum, b) => sum + (b.commission || 0), 0),
|
||||
|
||||
// 总计
|
||||
totalClicks: clickRecords.length,
|
||||
totalBindings: distributionBindings.length,
|
||||
totalConversions: distributionBindings.filter(b => b.status === 'converted').length,
|
||||
totalEarnings: distributionBindings
|
||||
.filter(b => b.status === 'converted')
|
||||
.reduce((sum, b) => sum + (b.commission || 0), 0),
|
||||
|
||||
// 即将过期
|
||||
expiringBindings: distributionBindings.filter(b =>
|
||||
b.status === 'active' &&
|
||||
new Date(b.expireTime) <= weekFromNow &&
|
||||
new Date(b.expireTime) > now
|
||||
).length,
|
||||
|
||||
// 待处理提现
|
||||
pendingWithdrawals: withdrawRecords.filter(w => w.status === 'pending').length,
|
||||
pendingWithdrawAmount: withdrawRecords
|
||||
.filter(w => w.status === 'pending')
|
||||
.reduce((sum, w) => sum + w.amount, 0),
|
||||
|
||||
// 转化率
|
||||
conversionRate: clickRecords.length > 0
|
||||
? (distributionBindings.filter(b => b.status === 'converted').length / clickRecords.length * 100).toFixed(2)
|
||||
: '0.00',
|
||||
|
||||
// 分销商数量
|
||||
totalDistributors: distributors.length,
|
||||
activeDistributors: distributors.filter(d => d.status === 'active').length,
|
||||
};
|
||||
|
||||
return NextResponse.json({ success: true, overview });
|
||||
}
|
||||
|
||||
// 获取绑定列表(后台)
|
||||
function getBindings(userId: string | null, page: number, pageSize: number) {
|
||||
let filteredBindings = [...distributionBindings];
|
||||
|
||||
if (userId) {
|
||||
filteredBindings = filteredBindings.filter(b => b.referrerId === userId);
|
||||
}
|
||||
|
||||
// 按创建时间倒序
|
||||
filteredBindings.sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
const total = filteredBindings.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const paginatedBindings = filteredBindings.slice(start, start + pageSize);
|
||||
|
||||
// 添加剩余天数
|
||||
const now = new Date();
|
||||
const bindingsWithDays = paginatedBindings.map(b => ({
|
||||
...b,
|
||||
daysRemaining: b.status === 'active'
|
||||
? Math.max(0, Math.ceil((new Date(b.expireTime).getTime() - now.getTime()) / (1000 * 60 * 60 * 24)))
|
||||
: 0,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
bindings: bindingsWithDays,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 获取我的绑定用户(分销中心)
|
||||
function getMyBindings(userId: string) {
|
||||
const myBindings = distributionBindings.filter(b => b.referrerId === userId);
|
||||
const now = new Date();
|
||||
|
||||
// 按状态分类
|
||||
const active = myBindings
|
||||
.filter(b => b.status === 'active')
|
||||
.map(b => ({
|
||||
...b,
|
||||
daysRemaining: Math.max(0, Math.ceil((new Date(b.expireTime).getTime() - now.getTime()) / (1000 * 60 * 60 * 24))),
|
||||
}))
|
||||
.sort((a, b) => a.daysRemaining - b.daysRemaining); // 即将过期的排前面
|
||||
|
||||
const converted = myBindings.filter(b => b.status === 'converted');
|
||||
const expired = myBindings.filter(b => b.status === 'expired');
|
||||
|
||||
// 统计
|
||||
const stats = {
|
||||
totalBindings: myBindings.length,
|
||||
activeCount: active.length,
|
||||
convertedCount: converted.length,
|
||||
expiredCount: expired.length,
|
||||
expiringCount: active.filter(b => b.daysRemaining <= 7).length,
|
||||
totalCommission: converted.reduce((sum, b) => sum + (b.commission || 0), 0),
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
bindings: {
|
||||
active,
|
||||
converted,
|
||||
expired,
|
||||
},
|
||||
stats,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取提现记录
|
||||
function getWithdrawals(userId: string | null, page: number, pageSize: number) {
|
||||
let filteredWithdrawals = [...withdrawRecords];
|
||||
|
||||
if (userId) {
|
||||
filteredWithdrawals = filteredWithdrawals.filter(w => w.userId === userId);
|
||||
}
|
||||
|
||||
// 按创建时间倒序
|
||||
filteredWithdrawals.sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
const total = filteredWithdrawals.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const paginatedWithdrawals = filteredWithdrawals.slice(start, start + pageSize);
|
||||
|
||||
// 统计
|
||||
const stats = {
|
||||
pending: filteredWithdrawals.filter(w => w.status === 'pending').length,
|
||||
pendingAmount: filteredWithdrawals
|
||||
.filter(w => w.status === 'pending')
|
||||
.reduce((sum, w) => sum + w.amount, 0),
|
||||
completed: filteredWithdrawals.filter(w => w.status === 'completed').length,
|
||||
completedAmount: filteredWithdrawals
|
||||
.filter(w => w.status === 'completed')
|
||||
.reduce((sum, w) => sum + w.amount, 0),
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
withdrawals: paginatedWithdrawals,
|
||||
stats,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 获取提醒
|
||||
function getReminders(userId: string) {
|
||||
const now = new Date();
|
||||
const weekFromNow = new Date();
|
||||
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
||||
|
||||
const myBindings = distributionBindings.filter(b =>
|
||||
b.referrerId === userId && b.status === 'active'
|
||||
);
|
||||
|
||||
const expiringSoon = myBindings.filter(b => {
|
||||
const expireTime = new Date(b.expireTime);
|
||||
return expireTime <= weekFromNow && expireTime > now;
|
||||
}).map(b => ({
|
||||
type: 'expiring_soon',
|
||||
binding: b,
|
||||
daysRemaining: Math.ceil((new Date(b.expireTime).getTime() - now.getTime()) / (1000 * 60 * 60 * 24)),
|
||||
message: `用户 ${b.visitorNickname || b.visitorPhone || '未知'} 的绑定将在 ${Math.ceil((new Date(b.expireTime).getTime() - now.getTime()) / (1000 * 60 * 60 * 24))} 天后过期`,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
reminders: expiringSoon,
|
||||
count: expiringSoon.length,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取分销商信息
|
||||
function getDistributor(userId: string) {
|
||||
const distributor = distributors.find(d => d.userId === userId);
|
||||
|
||||
if (!distributor) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
distributor: null,
|
||||
message: '用户尚未成为分销商',
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, distributor });
|
||||
}
|
||||
|
||||
// 获取排行榜
|
||||
function getRanking() {
|
||||
const ranking = [...distributors]
|
||||
.filter(d => d.status === 'active')
|
||||
.sort((a, b) => b.totalEarnings - a.totalEarnings)
|
||||
.slice(0, 10)
|
||||
.map((d, index) => ({
|
||||
rank: index + 1,
|
||||
distributorId: d.id,
|
||||
nickname: d.nickname,
|
||||
totalEarnings: d.totalEarnings,
|
||||
totalConversions: d.convertedBindings,
|
||||
level: d.level,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ success: true, ranking });
|
||||
}
|
||||
|
||||
// 记录点击
|
||||
function recordClick(body: {
|
||||
referralCode: string;
|
||||
referrerId: string;
|
||||
visitorId: string;
|
||||
visitorPhone?: string;
|
||||
visitorNickname?: string;
|
||||
source: 'link' | 'miniprogram' | 'poster' | 'qrcode';
|
||||
}) {
|
||||
const now = new Date();
|
||||
|
||||
// 1. 记录点击
|
||||
const click = {
|
||||
id: generateId('click_'),
|
||||
referralCode: body.referralCode,
|
||||
referrerId: body.referrerId,
|
||||
visitorId: body.visitorId,
|
||||
source: body.source,
|
||||
clickTime: now.toISOString(),
|
||||
};
|
||||
clickRecords.push(click);
|
||||
|
||||
// 2. 检查现有绑定
|
||||
const existingBinding = distributionBindings.find(b =>
|
||||
b.visitorId === body.visitorId &&
|
||||
b.status === 'active' &&
|
||||
new Date(b.expireTime) > now
|
||||
);
|
||||
|
||||
if (existingBinding) {
|
||||
// 已有有效绑定,只记录点击
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '点击已记录,用户已被其他分销商绑定',
|
||||
click,
|
||||
binding: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 创建新绑定
|
||||
const expireDate = new Date(now);
|
||||
expireDate.setDate(expireDate.getDate() + BINDING_DAYS);
|
||||
|
||||
const binding = {
|
||||
id: generateId('bind_'),
|
||||
referrerId: body.referrerId,
|
||||
referrerCode: body.referralCode,
|
||||
visitorId: body.visitorId,
|
||||
visitorPhone: body.visitorPhone,
|
||||
visitorNickname: body.visitorNickname,
|
||||
bindingTime: now.toISOString(),
|
||||
expireTime: expireDate.toISOString(),
|
||||
status: 'active' as const,
|
||||
source: body.source,
|
||||
createdAt: now.toISOString(),
|
||||
};
|
||||
distributionBindings.push(binding);
|
||||
|
||||
// 4. 更新分销商统计
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === body.referrerId);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].totalClicks++;
|
||||
distributors[distributorIndex].totalBindings++;
|
||||
distributors[distributorIndex].activeBindings++;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '点击已记录,绑定创建成功',
|
||||
click,
|
||||
binding,
|
||||
expireTime: expireDate.toISOString(),
|
||||
bindingDays: BINDING_DAYS,
|
||||
});
|
||||
}
|
||||
|
||||
// 转化绑定(用户付款)
|
||||
function convertBinding(body: {
|
||||
visitorId: string;
|
||||
orderId: string;
|
||||
orderAmount: number;
|
||||
}) {
|
||||
const now = new Date();
|
||||
|
||||
// 查找有效绑定
|
||||
const bindingIndex = distributionBindings.findIndex(b =>
|
||||
b.visitorId === body.visitorId &&
|
||||
b.status === 'active' &&
|
||||
new Date(b.expireTime) > now
|
||||
);
|
||||
|
||||
if (bindingIndex === -1) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
message: '未找到有效绑定,该订单不计入分销',
|
||||
});
|
||||
}
|
||||
|
||||
const binding = distributionBindings[bindingIndex];
|
||||
|
||||
// 查找分销商
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === binding.referrerId);
|
||||
const commissionRate = distributorIndex !== -1
|
||||
? distributors[distributorIndex].commissionRate
|
||||
: DEFAULT_COMMISSION_RATE;
|
||||
|
||||
const commission = body.orderAmount * (commissionRate / 100);
|
||||
|
||||
// 更新绑定
|
||||
distributionBindings[bindingIndex] = {
|
||||
...binding,
|
||||
status: 'converted',
|
||||
convertedAt: now.toISOString(),
|
||||
orderId: body.orderId,
|
||||
orderAmount: body.orderAmount,
|
||||
commission,
|
||||
};
|
||||
|
||||
// 更新分销商
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].activeBindings--;
|
||||
distributors[distributorIndex].convertedBindings++;
|
||||
distributors[distributorIndex].totalEarnings += commission;
|
||||
distributors[distributorIndex].pendingEarnings += commission;
|
||||
|
||||
// 检查是否需要自动提现
|
||||
checkAutoWithdraw(distributors[distributorIndex]);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '订单转化成功',
|
||||
binding: distributionBindings[bindingIndex],
|
||||
commission,
|
||||
referrerId: binding.referrerId,
|
||||
});
|
||||
}
|
||||
|
||||
// 申请提现
|
||||
function requestWithdraw(body: {
|
||||
userId: string;
|
||||
amount: number;
|
||||
method: 'wechat' | 'alipay';
|
||||
account: string;
|
||||
accountName: string;
|
||||
}) {
|
||||
// 查找分销商
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === body.userId);
|
||||
|
||||
if (distributorIndex === -1) {
|
||||
return NextResponse.json({ success: false, error: '分销商不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
const distributor = distributors[distributorIndex];
|
||||
|
||||
if (body.amount < MIN_WITHDRAW_AMOUNT) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `最低提现金额为 ${MIN_WITHDRAW_AMOUNT} 元`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
if (body.amount > distributor.pendingEarnings) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '提现金额超过可提现余额'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 创建提现记录
|
||||
const withdrawal = {
|
||||
id: generateId('withdraw_'),
|
||||
distributorId: distributor.id,
|
||||
userId: body.userId,
|
||||
userName: distributor.nickname,
|
||||
amount: body.amount,
|
||||
fee: 0,
|
||||
actualAmount: body.amount,
|
||||
method: body.method,
|
||||
account: body.account,
|
||||
accountName: body.accountName,
|
||||
status: 'pending' as const,
|
||||
isAuto: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
withdrawRecords.push(withdrawal);
|
||||
|
||||
// 扣除待提现金额
|
||||
distributors[distributorIndex].pendingEarnings -= body.amount;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '提现申请已提交',
|
||||
withdrawal,
|
||||
});
|
||||
}
|
||||
|
||||
// 设置自动提现
|
||||
function setAutoWithdraw(body: {
|
||||
userId: string;
|
||||
enabled: boolean;
|
||||
threshold?: number;
|
||||
account?: {
|
||||
type: 'wechat' | 'alipay';
|
||||
account: string;
|
||||
name: string;
|
||||
};
|
||||
}) {
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === body.userId);
|
||||
|
||||
if (distributorIndex === -1) {
|
||||
return NextResponse.json({ success: false, error: '分销商不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
distributors[distributorIndex] = {
|
||||
...distributors[distributorIndex],
|
||||
autoWithdraw: body.enabled,
|
||||
autoWithdrawThreshold: body.threshold || distributors[distributorIndex].autoWithdrawThreshold,
|
||||
autoWithdrawAccount: body.account || distributors[distributorIndex].autoWithdrawAccount,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: body.enabled ? '自动提现已开启' : '自动提现已关闭',
|
||||
distributor: distributors[distributorIndex],
|
||||
});
|
||||
}
|
||||
|
||||
// 处理过期绑定
|
||||
function processExpiredBindings() {
|
||||
const now = new Date();
|
||||
let expiredCount = 0;
|
||||
|
||||
distributionBindings.forEach((binding, index) => {
|
||||
if (binding.status === 'active' && new Date(binding.expireTime) <= now) {
|
||||
distributionBindings[index].status = 'expired';
|
||||
expiredCount++;
|
||||
|
||||
// 更新分销商统计
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === binding.referrerId);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].activeBindings--;
|
||||
distributors[distributorIndex].expiredBindings++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `已处理 ${expiredCount} 个过期绑定`,
|
||||
expiredCount,
|
||||
});
|
||||
}
|
||||
|
||||
// 审核通过提现
|
||||
async function approveWithdraw(body: { withdrawalId: string; reviewedBy?: string }) {
|
||||
const withdrawalIndex = withdrawRecords.findIndex(w => w.id === body.withdrawalId);
|
||||
|
||||
if (withdrawalIndex === -1) {
|
||||
return NextResponse.json({ success: false, error: '提现记录不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
const withdrawal = withdrawRecords[withdrawalIndex];
|
||||
|
||||
if (withdrawal.status !== 'pending') {
|
||||
return NextResponse.json({ success: false, error: '该提现申请已处理' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 更新状态为处理中
|
||||
withdrawRecords[withdrawalIndex].status = 'processing';
|
||||
|
||||
// 模拟打款(实际项目中调用支付接口)
|
||||
try {
|
||||
// 模拟延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 打款成功
|
||||
withdrawRecords[withdrawalIndex] = {
|
||||
...withdrawRecords[withdrawalIndex],
|
||||
status: 'completed',
|
||||
paymentNo: `PAY${Date.now()}`,
|
||||
paymentTime: new Date().toISOString(),
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 更新分销商已提现金额
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].withdrawnEarnings += withdrawal.amount;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '打款成功',
|
||||
withdrawal: withdrawRecords[withdrawalIndex],
|
||||
});
|
||||
} catch (error) {
|
||||
// 打款失败,退还金额
|
||||
withdrawRecords[withdrawalIndex].status = 'failed';
|
||||
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].pendingEarnings += withdrawal.amount;
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false, error: '打款失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝提现
|
||||
function rejectWithdraw(body: { withdrawalId: string; reason: string; reviewedBy?: string }) {
|
||||
const withdrawalIndex = withdrawRecords.findIndex(w => w.id === body.withdrawalId);
|
||||
|
||||
if (withdrawalIndex === -1) {
|
||||
return NextResponse.json({ success: false, error: '提现记录不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
const withdrawal = withdrawRecords[withdrawalIndex];
|
||||
|
||||
if (withdrawal.status !== 'pending') {
|
||||
return NextResponse.json({ success: false, error: '该提现申请已处理' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
withdrawRecords[withdrawalIndex] = {
|
||||
...withdrawal,
|
||||
status: 'rejected',
|
||||
reviewNote: body.reason,
|
||||
};
|
||||
|
||||
// 退还金额
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].pendingEarnings += withdrawal.amount;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '提现申请已拒绝',
|
||||
withdrawal: withdrawRecords[withdrawalIndex],
|
||||
});
|
||||
}
|
||||
|
||||
// 更新分销商信息
|
||||
function updateDistributor(body: {
|
||||
userId: string;
|
||||
commissionRate?: number;
|
||||
level?: 'normal' | 'silver' | 'gold' | 'diamond';
|
||||
status?: 'active' | 'frozen' | 'disabled';
|
||||
}) {
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === body.userId);
|
||||
|
||||
if (distributorIndex === -1) {
|
||||
return NextResponse.json({ success: false, error: '分销商不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
distributors[distributorIndex] = {
|
||||
...distributors[distributorIndex],
|
||||
...(body.commissionRate !== undefined && { commissionRate: body.commissionRate }),
|
||||
...(body.level && { level: body.level }),
|
||||
...(body.status && { status: body.status }),
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '分销商信息已更新',
|
||||
distributor: distributors[distributorIndex],
|
||||
});
|
||||
}
|
||||
|
||||
// 检查自动提现
|
||||
function checkAutoWithdraw(distributor: typeof distributors[0]) {
|
||||
if (!distributor.autoWithdraw || !distributor.autoWithdrawAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (distributor.pendingEarnings >= distributor.autoWithdrawThreshold) {
|
||||
// 创建自动提现记录
|
||||
const withdrawal = {
|
||||
id: generateId('withdraw_'),
|
||||
distributorId: distributor.id,
|
||||
userId: distributor.userId,
|
||||
userName: distributor.nickname,
|
||||
amount: distributor.pendingEarnings,
|
||||
fee: 0,
|
||||
actualAmount: distributor.pendingEarnings,
|
||||
method: distributor.autoWithdrawAccount.type,
|
||||
account: distributor.autoWithdrawAccount.account,
|
||||
accountName: distributor.autoWithdrawAccount.name,
|
||||
status: 'processing' as const,
|
||||
isAuto: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
withdrawRecords.push(withdrawal);
|
||||
|
||||
// 扣除待提现金额
|
||||
const distributorIndex = distributors.findIndex(d => d.id === distributor.id);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].pendingEarnings = 0;
|
||||
}
|
||||
|
||||
// 模拟打款(实际项目中调用支付接口)
|
||||
processAutoWithdraw(withdrawal.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理自动提现打款
|
||||
async function processAutoWithdraw(withdrawalId: string) {
|
||||
const withdrawalIndex = withdrawRecords.findIndex(w => w.id === withdrawalId);
|
||||
if (withdrawalIndex === -1) return;
|
||||
|
||||
try {
|
||||
// 模拟延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 打款成功
|
||||
const withdrawal = withdrawRecords[withdrawalIndex];
|
||||
withdrawRecords[withdrawalIndex] = {
|
||||
...withdrawal,
|
||||
status: 'completed',
|
||||
paymentNo: `AUTO_PAY${Date.now()}`,
|
||||
paymentTime: new Date().toISOString(),
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 更新分销商已提现金额
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].withdrawnEarnings += withdrawal.amount;
|
||||
}
|
||||
|
||||
console.log(`自动提现成功: ${withdrawal.userName}, 金额: ¥${withdrawal.amount}`);
|
||||
} catch (error) {
|
||||
// 打款失败
|
||||
withdrawRecords[withdrawalIndex].status = 'failed';
|
||||
|
||||
const withdrawal = withdrawRecords[withdrawalIndex];
|
||||
const distributorIndex = distributors.findIndex(d => d.userId === withdrawal.userId);
|
||||
if (distributorIndex !== -1) {
|
||||
distributors[distributorIndex].pendingEarnings += withdrawal.amount;
|
||||
}
|
||||
|
||||
console.error(`自动提现失败: ${withdrawal.userName}`);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 订单管理接口
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -13,7 +18,7 @@ export async function GET(request: NextRequest) {
|
||||
// For now, return mock data
|
||||
const orders = []
|
||||
|
||||
console.log("[v0] Fetching orders for user:", userId)
|
||||
console.log("[Karuo] Fetching orders for user:", userId)
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
@@ -21,7 +26,7 @@ export async function GET(request: NextRequest) {
|
||||
data: orders,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[v0] Get orders error:", error)
|
||||
console.error("[Karuo] Get orders error:", error)
|
||||
return NextResponse.json({ code: 500, message: "服务器错误" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
/**
|
||||
* 支付宝回调通知 API
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* POST /api/payment/alipay/notify
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { AlipayService } from "@/lib/payment/alipay"
|
||||
import { PaymentFactory, SignatureError } from "@/lib/payment"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/alipay"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// 获取表单数据
|
||||
const formData = await request.formData()
|
||||
const params: Record<string, string> = {}
|
||||
|
||||
@@ -10,39 +21,54 @@ export async function POST(request: NextRequest) {
|
||||
params[key] = value.toString()
|
||||
})
|
||||
|
||||
// 初始化支付宝服务
|
||||
const alipay = new AlipayService({
|
||||
appId: process.env.ALIPAY_APP_ID || "wx432c93e275548671",
|
||||
partnerId: process.env.ALIPAY_PARTNER_ID || "2088511801157159",
|
||||
key: process.env.ALIPAY_KEY || "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
|
||||
returnUrl: "",
|
||||
notifyUrl: "",
|
||||
console.log("[Alipay Notify] 收到回调:", {
|
||||
out_trade_no: params.out_trade_no,
|
||||
trade_status: params.trade_status,
|
||||
total_amount: params.total_amount,
|
||||
})
|
||||
|
||||
// 验证签名
|
||||
const isValid = alipay.verifySign(params)
|
||||
// 创建支付宝网关
|
||||
const gateway = PaymentFactory.create("alipay_wap")
|
||||
|
||||
if (!isValid) {
|
||||
console.error("[v0] Alipay signature verification failed")
|
||||
return new NextResponse("fail")
|
||||
try {
|
||||
// 解析并验证回调数据
|
||||
const notifyResult = gateway.parseNotify(params)
|
||||
|
||||
if (notifyResult.status === "paid") {
|
||||
console.log("[Alipay Notify] 支付成功:", {
|
||||
tradeSn: notifyResult.tradeSn,
|
||||
platformSn: notifyResult.platformSn,
|
||||
amount: notifyResult.payAmount / 100, // 转换为元
|
||||
payTime: notifyResult.payTime,
|
||||
})
|
||||
|
||||
// TODO: 更新订单状态
|
||||
// await OrderService.updateStatus(notifyResult.tradeSn, 'paid')
|
||||
|
||||
// TODO: 解锁内容/开通权限
|
||||
// await ContentService.unlockForUser(notifyResult.attach?.userId, notifyResult.attach?.productId)
|
||||
|
||||
// TODO: 分配佣金(如果有推荐人)
|
||||
// if (notifyResult.attach?.referralCode) {
|
||||
// await ReferralService.distributeCommission(notifyResult)
|
||||
// }
|
||||
} else {
|
||||
console.log("[Alipay Notify] 非支付成功状态:", notifyResult.status)
|
||||
}
|
||||
|
||||
// 返回成功响应
|
||||
return new NextResponse(gateway.successResponse())
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof SignatureError) {
|
||||
console.error("[Alipay Notify] 签名验证失败")
|
||||
return new NextResponse(gateway.failResponse())
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
const { out_trade_no, trade_status, buyer_id, total_amount } = params
|
||||
|
||||
// 只处理支付成功的通知
|
||||
if (trade_status === "TRADE_SUCCESS" || trade_status === "TRADE_FINISHED") {
|
||||
console.log("[v0] Alipay payment success:", {
|
||||
orderId: out_trade_no,
|
||||
amount: total_amount,
|
||||
buyerId: buyer_id,
|
||||
})
|
||||
|
||||
// TODO: 更新订单状态、解锁内容、分配佣金
|
||||
}
|
||||
|
||||
return new NextResponse("success")
|
||||
} catch (error) {
|
||||
console.error("[v0] Alipay notify error:", error)
|
||||
console.error("[Alipay Notify] 处理失败:", error)
|
||||
return new NextResponse("fail")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 支付回调接口
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -5,7 +10,7 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const { orderId, status, transactionId, amount, paymentMethod, signature } = body
|
||||
|
||||
console.log("[v0] Payment callback received:", {
|
||||
console.log("[Karuo] Payment callback received:", {
|
||||
orderId,
|
||||
status,
|
||||
transactionId,
|
||||
@@ -32,7 +37,7 @@ export async function POST(request: NextRequest) {
|
||||
// Update order status
|
||||
if (status === "success") {
|
||||
// Grant access
|
||||
console.log("[v0] Payment successful, granting access for order:", orderId)
|
||||
console.log("[Karuo] Payment successful, granting access for order:", orderId)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -40,7 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
message: "回调处理成功",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[v0] Payment callback error:", error)
|
||||
console.error("[Karuo] Payment callback error:", error)
|
||||
return NextResponse.json({ code: 500, message: "服务器错误" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
/**
|
||||
* 创建支付订单 API
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* POST /api/payment/create-order
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { AlipayService } from "@/lib/payment/alipay"
|
||||
import { WechatPayService } from "@/lib/payment/wechat"
|
||||
import {
|
||||
PaymentFactory,
|
||||
generateOrderSn,
|
||||
generateTradeSn,
|
||||
yuanToFen,
|
||||
getNotifyUrl,
|
||||
getReturnUrl,
|
||||
} from "@/lib/payment"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/alipay"
|
||||
import "@/lib/payment/wechat"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { userId, type, sectionId, sectionTitle, amount, paymentMethod, referralCode } = body
|
||||
|
||||
// Validate required fields
|
||||
// 验证必要参数
|
||||
if (!userId || !type || !amount || !paymentMethod) {
|
||||
return NextResponse.json({ code: 400, message: "缺少必要参数" }, { status: 400 })
|
||||
return NextResponse.json(
|
||||
{ code: 400, message: "缺少必要参数", data: null },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Generate order ID
|
||||
const orderId = `ORDER_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
// 生成订单号
|
||||
const orderSn = generateOrderSn()
|
||||
const tradeSn = generateTradeSn()
|
||||
|
||||
// Create order object
|
||||
// 创建订单对象
|
||||
const order = {
|
||||
orderId,
|
||||
orderSn,
|
||||
tradeSn,
|
||||
userId,
|
||||
type, // "section" | "fullbook"
|
||||
sectionId: type === "section" ? sectionId : undefined,
|
||||
@@ -25,64 +47,83 @@ export async function POST(request: NextRequest) {
|
||||
amount,
|
||||
paymentMethod, // "wechat" | "alipay" | "usdt" | "paypal"
|
||||
referralCode,
|
||||
status: "pending", // pending | completed | failed | refunded
|
||||
status: "created",
|
||||
createdAt: new Date().toISOString(),
|
||||
expireAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30 minutes
|
||||
expireAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30分钟
|
||||
}
|
||||
|
||||
// According to the payment method, create a payment order
|
||||
// 获取客户端IP
|
||||
const clientIp = request.headers.get("x-forwarded-for")
|
||||
|| request.headers.get("x-real-ip")
|
||||
|| "127.0.0.1"
|
||||
|
||||
// 根据支付方式创建支付网关
|
||||
let paymentData = null
|
||||
const goodsTitle = type === "section" ? `购买章节: ${sectionTitle}` : "购买整本书"
|
||||
|
||||
// 确定网关类型
|
||||
let gateway: string
|
||||
if (paymentMethod === "alipay") {
|
||||
const alipay = new AlipayService({
|
||||
appId: process.env.ALIPAY_APP_ID || "wx432c93e275548671",
|
||||
partnerId: process.env.ALIPAY_PARTNER_ID || "2088511801157159",
|
||||
key: process.env.ALIPAY_KEY || "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
|
||||
returnUrl: process.env.ALIPAY_RETURN_URL || `${process.env.NEXT_PUBLIC_BASE_URL}/payment/success`,
|
||||
notifyUrl: process.env.ALIPAY_NOTIFY_URL || `${process.env.NEXT_PUBLIC_BASE_URL}/api/payment/alipay/notify`,
|
||||
})
|
||||
|
||||
paymentData = alipay.createOrder({
|
||||
outTradeNo: orderId,
|
||||
subject: type === "section" ? `购买章节: ${sectionTitle}` : "购买整本书",
|
||||
totalAmount: amount,
|
||||
body: `知识付费-书籍购买`,
|
||||
})
|
||||
gateway = "alipay_wap"
|
||||
} else if (paymentMethod === "wechat") {
|
||||
const wechat = new WechatPayService({
|
||||
appId: process.env.WECHAT_APP_ID || "wx432c93e275548671",
|
||||
appSecret: process.env.WECHAT_APP_SECRET || "25b7e7fdb7998e5107e242ebb6ddabd0",
|
||||
mchId: process.env.WECHAT_MCH_ID || "1318592501",
|
||||
apiKey: process.env.WECHAT_API_KEY || "wx3e31b068be59ddc131b068be59ddc2",
|
||||
notifyUrl: process.env.WECHAT_NOTIFY_URL || `${process.env.NEXT_PUBLIC_BASE_URL}/api/payment/wechat/notify`,
|
||||
gateway = "wechat_native"
|
||||
} else {
|
||||
gateway = paymentMethod
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建支付网关
|
||||
const paymentGateway = PaymentFactory.create(gateway)
|
||||
|
||||
// 创建交易
|
||||
const tradeResult = await paymentGateway.createTrade({
|
||||
goodsTitle,
|
||||
goodsDetail: `知识付费-书籍购买`,
|
||||
tradeSn,
|
||||
orderSn,
|
||||
amount: yuanToFen(amount),
|
||||
notifyUrl: getNotifyUrl(paymentMethod === "wechat" ? "wechat" : "alipay"),
|
||||
returnUrl: getReturnUrl(orderSn),
|
||||
createIp: clientIp.split(",")[0].trim(),
|
||||
platformType: paymentMethod === "wechat" ? "native" : "wap",
|
||||
})
|
||||
|
||||
const clientIp = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "127.0.0.1"
|
||||
paymentData = {
|
||||
type: tradeResult.type,
|
||||
payload: tradeResult.payload,
|
||||
tradeSn: tradeResult.tradeSn,
|
||||
expiration: tradeResult.expiration,
|
||||
}
|
||||
|
||||
paymentData = await wechat.createOrder({
|
||||
outTradeNo: orderId,
|
||||
body: type === "section" ? `购买章节: ${sectionTitle}` : "购买整本书",
|
||||
totalFee: amount,
|
||||
spbillCreateIp: clientIp.split(",")[0].trim(),
|
||||
})
|
||||
} catch (gatewayError) {
|
||||
console.error("[Payment] Gateway error:", gatewayError)
|
||||
// 如果网关创建失败,返回模拟数据(开发测试用)
|
||||
paymentData = {
|
||||
type: "qrcode",
|
||||
payload: `mock://pay/${paymentMethod}/${orderSn}`,
|
||||
tradeSn,
|
||||
expiration: 1800,
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
code: 0,
|
||||
code: 200,
|
||||
message: "订单创建成功",
|
||||
data: {
|
||||
...order,
|
||||
paymentData,
|
||||
gateway, // 返回网关信息,用于前端轮询时指定
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[v0] Create order error:", error)
|
||||
console.error("[Payment] Create order error:", error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: error instanceof Error ? error.message : "服务器错误",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
37
app/api/payment/methods/route.ts
Normal file
37
app/api/payment/methods/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 获取可用支付方式 API
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* GET /api/payment/methods
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { PaymentFactory } from "@/lib/payment"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/alipay"
|
||||
import "@/lib/payment/wechat"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const methods = PaymentFactory.getEnabledGateways()
|
||||
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
methods,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[Payment] Get methods error:", error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: error instanceof Error ? error.message : "服务器错误",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
143
app/api/payment/query/route.ts
Normal file
143
app/api/payment/query/route.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 查询支付状态 API
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* GET /api/payment/query?tradeSn=xxx&gateway=wechat_native|alipay_wap
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { PaymentFactory } from "@/lib/payment"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/alipay"
|
||||
import "@/lib/payment/wechat"
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tradeSn = searchParams.get("tradeSn")
|
||||
const gateway = searchParams.get("gateway") // 可选:指定支付网关
|
||||
|
||||
if (!tradeSn) {
|
||||
return NextResponse.json(
|
||||
{ code: 400, message: "缺少交易号", data: null },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log("[Payment Query] 查询交易状态:", { tradeSn, gateway })
|
||||
|
||||
let wechatResult = null
|
||||
let alipayResult = null
|
||||
|
||||
// 如果指定了网关,只查询该网关
|
||||
if (gateway) {
|
||||
try {
|
||||
const paymentGateway = PaymentFactory.create(gateway)
|
||||
const result = await paymentGateway.queryTrade(tradeSn)
|
||||
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
tradeSn: result?.tradeSn || tradeSn,
|
||||
status: result?.status || "paying",
|
||||
platformSn: result?.platformSn || null,
|
||||
payAmount: result?.payAmount || null,
|
||||
payTime: result?.payTime || null,
|
||||
gateway,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(`[Payment Query] ${gateway} 查询失败:`, e)
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
tradeSn,
|
||||
status: "paying",
|
||||
platformSn: null,
|
||||
payAmount: null,
|
||||
payTime: null,
|
||||
gateway,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 没有指定网关时,先查询微信
|
||||
try {
|
||||
const wechatGateway = PaymentFactory.create("wechat_native")
|
||||
wechatResult = await wechatGateway.queryTrade(tradeSn)
|
||||
|
||||
if (wechatResult && wechatResult.status === "paid") {
|
||||
console.log("[Payment Query] 微信支付成功:", { tradeSn, status: wechatResult.status })
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
tradeSn: wechatResult.tradeSn,
|
||||
status: wechatResult.status,
|
||||
platformSn: wechatResult.platformSn,
|
||||
payAmount: wechatResult.payAmount,
|
||||
payTime: wechatResult.payTime,
|
||||
gateway: "wechat_native",
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Payment Query] 微信查询异常:", e)
|
||||
}
|
||||
|
||||
// 再查询支付宝
|
||||
try {
|
||||
const alipayGateway = PaymentFactory.create("alipay_wap")
|
||||
alipayResult = await alipayGateway.queryTrade(tradeSn)
|
||||
|
||||
if (alipayResult && alipayResult.status === "paid") {
|
||||
console.log("[Payment Query] 支付宝支付成功:", { tradeSn, status: alipayResult.status })
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
tradeSn: alipayResult.tradeSn,
|
||||
status: alipayResult.status,
|
||||
platformSn: alipayResult.platformSn,
|
||||
payAmount: alipayResult.payAmount,
|
||||
payTime: alipayResult.payTime,
|
||||
gateway: "alipay_wap",
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Payment Query] 支付宝查询异常:", e)
|
||||
}
|
||||
|
||||
// 如果都未支付,优先返回微信的状态(因为更可靠)
|
||||
const result = wechatResult || alipayResult
|
||||
|
||||
// 返回等待支付状态
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: {
|
||||
tradeSn,
|
||||
status: result?.status || "paying",
|
||||
platformSn: result?.platformSn || null,
|
||||
payAmount: result?.payAmount || null,
|
||||
payTime: result?.payTime || null,
|
||||
gateway: null,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[Payment Query] Error:", error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: error instanceof Error ? error.message : "服务器错误",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
53
app/api/payment/status/[orderSn]/route.ts
Normal file
53
app/api/payment/status/[orderSn]/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 查询订单支付状态 API
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* GET /api/payment/status/{orderSn}
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ orderSn: string }> }
|
||||
) {
|
||||
try {
|
||||
const { orderSn } = await params
|
||||
|
||||
if (!orderSn) {
|
||||
return NextResponse.json(
|
||||
{ code: 400, message: "缺少订单号", data: null },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: 从数据库查询订单状态
|
||||
// const order = await OrderService.getByOrderSn(orderSn)
|
||||
|
||||
// 模拟返回数据(开发测试用)
|
||||
const mockOrder = {
|
||||
orderSn,
|
||||
status: "created", // created | paying | paid | closed | refunded
|
||||
paidAmount: null,
|
||||
paidAt: null,
|
||||
paymentMethod: null,
|
||||
tradeSn: null,
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
message: "success",
|
||||
data: mockOrder,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[Payment] Query status error:", error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
code: 500,
|
||||
message: error instanceof Error ? error.message : "服务器错误",
|
||||
data: null,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 支付验证接口
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -11,7 +16,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// In production, verify with payment gateway API
|
||||
// For now, simulate verification
|
||||
console.log("[v0] Verifying payment:", { orderId, paymentMethod, transactionId })
|
||||
console.log("[Karuo] Verifying payment:", { orderId, paymentMethod, transactionId })
|
||||
|
||||
// Simulate verification delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
@@ -40,7 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[v0] Verify payment error:", error)
|
||||
console.error("[Karuo] Verify payment error:", error)
|
||||
return NextResponse.json({ code: 500, message: "服务器错误" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,74 @@
|
||||
/**
|
||||
* 微信支付回调通知 API
|
||||
* 基于 Universal_Payment_Module v4.0 设计
|
||||
*
|
||||
* POST /api/payment/wechat/notify
|
||||
*/
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { WechatPayService } from "@/lib/payment/wechat"
|
||||
import { PaymentFactory, SignatureError } from "@/lib/payment"
|
||||
|
||||
// 确保网关已注册
|
||||
import "@/lib/payment/wechat"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// 获取XML原始数据
|
||||
const xmlData = await request.text()
|
||||
|
||||
const wechat = new WechatPayService({
|
||||
appId: process.env.WECHAT_APP_ID || "wx432c93e275548671",
|
||||
appSecret: process.env.WECHAT_APP_SECRET || "25b7e7fdb7998e5107e242ebb6ddabd0",
|
||||
mchId: process.env.WECHAT_MCH_ID || "1318592501",
|
||||
apiKey: process.env.WECHAT_API_KEY || "wx3e31b068be59ddc131b068be59ddc2",
|
||||
notifyUrl: "",
|
||||
})
|
||||
console.log("[Wechat Notify] 收到回调:", xmlData.slice(0, 200))
|
||||
|
||||
// 解析XML数据
|
||||
const params = await wechat["parseXML"](xmlData)
|
||||
// 创建微信支付网关
|
||||
const gateway = PaymentFactory.create("wechat_native")
|
||||
|
||||
// 验证签名
|
||||
const isValid = wechat.verifySign(params)
|
||||
try {
|
||||
// 解析并验证回调数据
|
||||
const notifyResult = gateway.parseNotify(xmlData)
|
||||
|
||||
if (!isValid) {
|
||||
console.error("[v0] WeChat signature verification failed")
|
||||
return new NextResponse(
|
||||
"<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>",
|
||||
{
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
},
|
||||
)
|
||||
}
|
||||
if (notifyResult.status === "paid") {
|
||||
console.log("[Wechat Notify] 支付成功:", {
|
||||
tradeSn: notifyResult.tradeSn,
|
||||
platformSn: notifyResult.platformSn,
|
||||
amount: notifyResult.payAmount / 100, // 转换为元
|
||||
payTime: notifyResult.payTime,
|
||||
})
|
||||
|
||||
const { out_trade_no, result_code, total_fee, openid } = params
|
||||
// TODO: 更新订单状态
|
||||
// await OrderService.updateStatus(notifyResult.tradeSn, 'paid')
|
||||
|
||||
if (result_code === "SUCCESS") {
|
||||
console.log("[v0] WeChat payment success:", {
|
||||
orderId: out_trade_no,
|
||||
amount: Number.parseInt(total_fee) / 100,
|
||||
openid,
|
||||
// TODO: 解锁内容/开通权限
|
||||
// await ContentService.unlockForUser(notifyResult.attach?.userId, notifyResult.attach?.productId)
|
||||
|
||||
// TODO: 分配佣金(如果有推荐人)
|
||||
// if (notifyResult.attach?.referralCode) {
|
||||
// await ReferralService.distributeCommission(notifyResult)
|
||||
// }
|
||||
} else {
|
||||
console.log("[Wechat Notify] 支付失败:", notifyResult)
|
||||
}
|
||||
|
||||
// 返回成功响应
|
||||
return new NextResponse(gateway.successResponse(), {
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
})
|
||||
|
||||
// TODO: 更新订单状态、解锁内容、分配佣金
|
||||
} catch (error) {
|
||||
if (error instanceof SignatureError) {
|
||||
console.error("[Wechat Notify] 签名验证失败")
|
||||
return new NextResponse(gateway.failResponse(), {
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return new NextResponse(
|
||||
"<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>",
|
||||
{
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("[v0] WeChat notify error:", error)
|
||||
return new NextResponse(
|
||||
"<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[系统错误]]></return_msg></xml>",
|
||||
{
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
},
|
||||
)
|
||||
console.error("[Wechat Notify] 处理失败:", error)
|
||||
|
||||
// 返回失败响应
|
||||
const failXml = '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[系统错误]]></return_msg></xml>'
|
||||
return new NextResponse(failXml, {
|
||||
headers: { "Content-Type": "application/xml" },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronRight, Lock, Unlock, Book, BookOpen, Home, List, Sparkles, User } from "lucide-react"
|
||||
import { ChevronRight, Lock, Unlock, Book, BookOpen, Home, List, Sparkles, User, Users, Zap, Crown } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { bookData, getTotalSectionCount, specialSections } from "@/lib/book-data"
|
||||
import { bookData, getTotalSectionCount, specialSections, getPremiumBookPrice, getExtraSectionsCount, BASE_SECTIONS_COUNT } from "@/lib/book-data"
|
||||
|
||||
export default function ChaptersPage() {
|
||||
const router = useRouter()
|
||||
const { user, hasPurchased } = useStore()
|
||||
const [expandedPart, setExpandedPart] = useState<string | null>("part-1")
|
||||
const [bookVersion, setBookVersion] = useState<"basic" | "premium">("basic")
|
||||
const [showPremiumTab, setShowPremiumTab] = useState(false) // 控制是否显示最新完整版标签
|
||||
|
||||
const totalSections = getTotalSectionCount()
|
||||
const hasFullBook = user?.hasFullBook || false
|
||||
const premiumPrice = getPremiumBookPrice()
|
||||
const extraSections = getExtraSectionsCount()
|
||||
|
||||
const handleSectionClick = (sectionId: string) => {
|
||||
router.push(`/read/${sectionId}`)
|
||||
@@ -41,6 +45,33 @@ export default function ChaptersPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 版本标签切换 - 仅在showPremiumTab为true时显示 */}
|
||||
{showPremiumTab && extraSections > 0 && (
|
||||
<div className="mx-4 mt-3 flex gap-2">
|
||||
<button
|
||||
onClick={() => setBookVersion("basic")}
|
||||
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-all ${
|
||||
bookVersion === "basic"
|
||||
? "bg-[#00CED1]/20 text-[#00CED1] border border-[#00CED1]/30"
|
||||
: "bg-[#1c1c1e] text-white/60 border border-white/5"
|
||||
}`}
|
||||
>
|
||||
基础版 ¥9.9
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBookVersion("premium")}
|
||||
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-all flex items-center justify-center gap-1 ${
|
||||
bookVersion === "premium"
|
||||
? "bg-[#FFD700]/20 text-[#FFD700] border border-[#FFD700]/30"
|
||||
: "bg-[#1c1c1e] text-white/60 border border-white/5"
|
||||
}`}
|
||||
>
|
||||
<Crown className="w-4 h-4" />
|
||||
最新版 ¥{premiumPrice.toFixed(1)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 目录内容 */}
|
||||
<main className="px-4 py-4">
|
||||
@@ -177,12 +208,12 @@ export default function ChaptersPage() {
|
||||
<List className="w-5 h-5 text-[#00CED1] mb-1" />
|
||||
<span className="text-[#00CED1] text-xs font-medium">目录</span>
|
||||
</button>
|
||||
{/* 匹配按钮 - 更大更突出 */}
|
||||
{/* 找伙伴按钮 */}
|
||||
<button onClick={() => router.push("/match")} className="flex flex-col items-center py-2 px-6 -mt-4">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
|
||||
<Sparkles className="w-6 h-6 text-white" />
|
||||
<Users className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className="text-gray-500 text-xs mt-1">匹配</span>
|
||||
<span className="text-gray-500 text-xs mt-1">找伙伴</span>
|
||||
</button>
|
||||
<button onClick={() => router.push("/my")} className="flex flex-col items-center py-2 px-4">
|
||||
<User className="w-5 h-5 text-gray-500 mb-1" />
|
||||
|
||||
@@ -505,3 +505,15 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* 抑制 Next.js 开发环境错误遮罩 */
|
||||
nextjs-portal,
|
||||
#nextjs-portal,
|
||||
[data-nextjs-dialog-overlay],
|
||||
[data-nextjs-toast],
|
||||
.nextjs-static-indicator-base {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ const _geist = Geist({ subsets: ["latin"] })
|
||||
const _geistMono = Geist_Mono({ subsets: ["latin"] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "一场SOUL的创业实验场 - 卡若",
|
||||
title: "一场soul的创业实验 - 卡若",
|
||||
description: "来自Soul派对房的真实商业故事,每天早上6-9点免费分享",
|
||||
generator: "v0.app",
|
||||
authors: [{ name: "卡若", url: "https://soul.quwanzhi.com" }],
|
||||
keywords: ["创业", "商业案例", "Soul", "私域运营"],
|
||||
icons: {
|
||||
icon: [
|
||||
{
|
||||
@@ -38,7 +39,7 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className={`bg-[#0a1628]`}>
|
||||
<body className="bg-black">
|
||||
<LayoutWrapper>{children}</LayoutWrapper>
|
||||
<Analytics />
|
||||
</body>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { Mic, X, CheckCircle, Loader2 } from "lucide-react"
|
||||
import { Users, X, CheckCircle, Loader2, Lock, Zap, Gift } from "lucide-react"
|
||||
import { Home, List, User } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
interface MatchUser {
|
||||
id: string
|
||||
@@ -18,73 +19,131 @@ interface MatchUser {
|
||||
}
|
||||
|
||||
const matchTypes = [
|
||||
{ id: "partner", label: "创业合伙", icon: "⭐", color: "#00E5FF", joinable: false },
|
||||
{ id: "investor", label: "资源对接", icon: "👥", color: "#7B61FF", joinable: true },
|
||||
{ id: "mentor", label: "导师顾问", icon: "❤️", color: "#E91E63", joinable: true },
|
||||
{ id: "team", label: "团队招募", icon: "🎮", color: "#4CAF50", joinable: true },
|
||||
{ id: "partner", label: "创业合伙", matchLabel: "创业伙伴", icon: "⭐", color: "#00E5FF", matchFromDB: true, showJoinAfterMatch: false },
|
||||
{ id: "investor", label: "资源对接", matchLabel: "资源对接", icon: "👥", color: "#7B61FF", matchFromDB: false, showJoinAfterMatch: true },
|
||||
{ id: "mentor", label: "导师顾问", matchLabel: "商业顾问", icon: "❤️", color: "#E91E63", matchFromDB: false, showJoinAfterMatch: true },
|
||||
{ id: "team", label: "团队招募", matchLabel: "加入项目", icon: "🎮", color: "#4CAF50", matchFromDB: false, showJoinAfterMatch: true },
|
||||
]
|
||||
|
||||
// 获取本地存储的手机号
|
||||
const getStoredPhone = (): string => {
|
||||
const FREE_MATCH_LIMIT = 1 // 每日免费匹配次数改为1次
|
||||
const MATCH_UNLOCK_PRICE = 1 // 每次解锁需要购买1个小节
|
||||
|
||||
// 获取本地存储的联系方式
|
||||
const getStoredContact = (): { phone: string; wechat: string } => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("user_phone") || ""
|
||||
return {
|
||||
phone: localStorage.getItem("user_phone") || "",
|
||||
wechat: localStorage.getItem("user_wechat") || "",
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return { phone: "", wechat: "" }
|
||||
}
|
||||
|
||||
// 保存手机号到本地存储
|
||||
const savePhone = (phone: string) => {
|
||||
// 获取今日匹配次数
|
||||
const getTodayMatchCount = (): number => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("user_phone", phone)
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const stored = localStorage.getItem("match_count_data")
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored)
|
||||
if (data.date === today) {
|
||||
return data.count
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 保存今日匹配次数
|
||||
const saveTodayMatchCount = (count: number) => {
|
||||
if (typeof window !== "undefined") {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
localStorage.setItem("match_count_data", JSON.stringify({ date: today, count }))
|
||||
}
|
||||
}
|
||||
|
||||
// 保存联系方式到本地存储
|
||||
const saveContact = (phone: string, wechat: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
if (phone) localStorage.setItem("user_phone", phone)
|
||||
if (wechat) localStorage.setItem("user_wechat", wechat)
|
||||
}
|
||||
}
|
||||
|
||||
export default function MatchPage() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [isMatching, setIsMatching] = useState(false)
|
||||
const [currentMatch, setCurrentMatch] = useState<MatchUser | null>(null)
|
||||
const [matchAttempts, setMatchAttempts] = useState(0)
|
||||
const [selectedType, setSelectedType] = useState("partner")
|
||||
const [todayMatchCount, setTodayMatchCount] = useState(0)
|
||||
const router = useRouter()
|
||||
const { user, isLoggedIn, purchaseSection } = useStore()
|
||||
|
||||
const [showJoinModal, setShowJoinModal] = useState(false)
|
||||
const [showUnlockModal, setShowUnlockModal] = useState(false)
|
||||
const [joinType, setJoinType] = useState<string | null>(null)
|
||||
const [phoneNumber, setPhoneNumber] = useState("")
|
||||
const [wechatId, setWechatId] = useState("")
|
||||
const [contactType, setContactType] = useState<"phone" | "wechat">("phone")
|
||||
const [isJoining, setIsJoining] = useState(false)
|
||||
const [joinSuccess, setJoinSuccess] = useState(false)
|
||||
const [joinError, setJoinError] = useState("")
|
||||
const [isUnlocking, setIsUnlocking] = useState(false)
|
||||
|
||||
// 初始化时读取已存储的手机号
|
||||
// 检查用户是否有购买权限(购买过任意内容)
|
||||
const hasPurchased = user?.hasFullBook || (user?.purchasedSections && user.purchasedSections.length > 0)
|
||||
|
||||
// 总共获得的匹配次数 = 每日免费(1) + 已购小节数量
|
||||
// 如果购买了全书,则拥有无限匹配机会
|
||||
const totalMatchesAllowed = user?.hasFullBook ? 999999 : FREE_MATCH_LIMIT + (user?.purchasedSections?.length || 0)
|
||||
|
||||
// 剩余可用次数
|
||||
const matchesRemaining = user?.hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - todayMatchCount)
|
||||
|
||||
// 是否需要付费(总次数用完)
|
||||
const needPayToMatch = !user?.hasFullBook && matchesRemaining <= 0
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
const storedPhone = getStoredPhone()
|
||||
if (storedPhone) {
|
||||
setPhoneNumber(storedPhone)
|
||||
setMounted(true)
|
||||
const storedContact = getStoredContact()
|
||||
if (storedContact.phone) {
|
||||
setPhoneNumber(storedContact.phone)
|
||||
}
|
||||
}, [])
|
||||
if (storedContact.wechat) {
|
||||
setWechatId(storedContact.wechat)
|
||||
}
|
||||
if (user?.phone) {
|
||||
setPhoneNumber(user.phone)
|
||||
}
|
||||
|
||||
// 读取今日匹配次数
|
||||
setTodayMatchCount(getTodayMatchCount())
|
||||
}, [user])
|
||||
|
||||
if (!mounted) return null // 彻底解决 Hydration 错误
|
||||
|
||||
const handleJoinClick = (typeId: string) => {
|
||||
const type = matchTypes.find((t) => t.id === typeId)
|
||||
if (type?.joinable) {
|
||||
setJoinType(typeId)
|
||||
setShowJoinModal(true)
|
||||
setJoinSuccess(false)
|
||||
setJoinError("")
|
||||
// 如果有存储的手机号,自动填充
|
||||
const storedPhone = getStoredPhone()
|
||||
if (storedPhone) {
|
||||
setPhoneNumber(storedPhone)
|
||||
}
|
||||
} else {
|
||||
// 不可加入的类型,直接选中并开始匹配
|
||||
setSelectedType(typeId)
|
||||
}
|
||||
setJoinType(typeId)
|
||||
setShowJoinModal(true)
|
||||
setJoinSuccess(false)
|
||||
setJoinError("")
|
||||
}
|
||||
|
||||
const handleJoinSubmit = async () => {
|
||||
if (!phoneNumber || phoneNumber.length !== 11) {
|
||||
const contact = contactType === "phone" ? phoneNumber : wechatId
|
||||
|
||||
if (contactType === "phone" && (!phoneNumber || phoneNumber.length !== 11)) {
|
||||
setJoinError("请输入正确的11位手机号")
|
||||
return
|
||||
}
|
||||
|
||||
if (contactType === "wechat" && (!wechatId || wechatId.length < 6)) {
|
||||
setJoinError("请输入正确的微信号(至少6位)")
|
||||
return
|
||||
}
|
||||
|
||||
setIsJoining(true)
|
||||
setJoinError("")
|
||||
|
||||
@@ -96,17 +155,17 @@ export default function MatchPage() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: joinType,
|
||||
phone: phoneNumber,
|
||||
phone: contactType === "phone" ? phoneNumber : "",
|
||||
wechat: contactType === "wechat" ? wechatId : "",
|
||||
userId: user?.id,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
// 保存手机号以便下次使用
|
||||
savePhone(phoneNumber)
|
||||
saveContact(phoneNumber, wechatId)
|
||||
setJoinSuccess(true)
|
||||
// 2秒后关闭弹窗
|
||||
setTimeout(() => {
|
||||
setShowJoinModal(false)
|
||||
setJoinSuccess(false)
|
||||
@@ -121,7 +180,69 @@ export default function MatchPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 购买解锁匹配次数
|
||||
const handleUnlockMatch = async () => {
|
||||
if (!isLoggedIn) {
|
||||
alert("请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
setIsUnlocking(true)
|
||||
try {
|
||||
// 模拟购买过程,实际应该调用支付API
|
||||
// 这里简化为直接购买成功
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// 购买成功后重置今日匹配次数(增加3次)
|
||||
const newCount = Math.max(0, todayMatchCount - 3)
|
||||
saveTodayMatchCount(newCount)
|
||||
setTodayMatchCount(newCount)
|
||||
|
||||
setShowUnlockModal(false)
|
||||
alert("解锁成功!已获得3次匹配机会")
|
||||
} catch (error) {
|
||||
alert("解锁失败,请重试")
|
||||
} finally {
|
||||
setIsUnlocking(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 上报匹配行为到CKB
|
||||
const reportMatchToCKB = async (matchedUser: MatchUser) => {
|
||||
try {
|
||||
await fetch("/api/ckb/match", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
matchType: selectedType,
|
||||
phone: phoneNumber || user?.phone || "",
|
||||
wechat: wechatId || user?.wechat || "",
|
||||
userId: user?.id || "",
|
||||
nickname: user?.nickname || "",
|
||||
matchedUser: {
|
||||
id: matchedUser.id,
|
||||
nickname: matchedUser.nickname,
|
||||
matchScore: matchedUser.matchScore,
|
||||
},
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("上报匹配失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const startMatch = () => {
|
||||
// 检查是否有购买权限
|
||||
if (!hasPurchased) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要付费
|
||||
if (needPayToMatch) {
|
||||
setShowUnlockModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
setIsMatching(true)
|
||||
setMatchAttempts(0)
|
||||
setCurrentMatch(null)
|
||||
@@ -134,7 +255,25 @@ export default function MatchPage() {
|
||||
() => {
|
||||
clearInterval(interval)
|
||||
setIsMatching(false)
|
||||
setCurrentMatch(getMockMatch())
|
||||
const matchedUser = getMockMatch()
|
||||
setCurrentMatch(matchedUser)
|
||||
|
||||
// 增加今日匹配次数
|
||||
const newCount = todayMatchCount + 1
|
||||
setTodayMatchCount(newCount)
|
||||
saveTodayMatchCount(newCount)
|
||||
|
||||
// 上报匹配行为
|
||||
reportMatchToCKB(matchedUser)
|
||||
|
||||
// 如果是需要弹出加入弹窗的类型,自动弹出
|
||||
const currentType = matchTypes.find(t => t.id === selectedType)
|
||||
if (currentType?.showJoinAfterMatch) {
|
||||
setJoinType(selectedType)
|
||||
setShowJoinModal(true)
|
||||
setJoinSuccess(false)
|
||||
setJoinError("")
|
||||
}
|
||||
},
|
||||
Math.random() * 3000 + 3000,
|
||||
)
|
||||
@@ -167,6 +306,12 @@ export default function MatchPage() {
|
||||
}
|
||||
|
||||
const nextMatch = () => {
|
||||
// 检查是否需要付费
|
||||
if (needPayToMatch) {
|
||||
setShowUnlockModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentMatch(null)
|
||||
setTimeout(() => startMatch(), 500)
|
||||
}
|
||||
@@ -183,13 +328,15 @@ export default function MatchPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const currentTypeLabel = matchTypes.find((t) => t.id === selectedType)?.label || "创业合伙"
|
||||
const joinTypeLabel = matchTypes.find((t) => t.id === joinType)?.label || ""
|
||||
const currentType = matchTypes.find((t) => t.id === selectedType)
|
||||
const currentTypeLabel = currentType?.label || "创业合伙"
|
||||
const currentMatchLabel = currentType?.matchLabel || "创业伙伴"
|
||||
const joinTypeLabel = matchTypes.find((t) => t.id === joinType)?.matchLabel || ""
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black pb-24">
|
||||
<div className="flex items-center justify-between px-6 pt-6 pb-4">
|
||||
<h1 className="text-2xl font-bold text-white">语音匹配</h1>
|
||||
<h1 className="text-2xl font-bold text-white">找伙伴</h1>
|
||||
<button className="w-10 h-10 rounded-full bg-[#1c1c1e] flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-white/60" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
@@ -202,6 +349,33 @@ export default function MatchPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 今日匹配次数显示 - 仅在总次数用完时显示 */}
|
||||
{hasPurchased && (
|
||||
<div className="px-6 mb-4">
|
||||
<div className={`flex items-center justify-between p-3 rounded-xl bg-[#1c1c1e] border ${matchesRemaining <= 0 && !user?.hasFullBook ? 'border-[#FFD700]/20' : 'border-white/5'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className={`w-5 h-5 ${matchesRemaining <= 0 && !user?.hasFullBook ? 'text-[#FFD700]' : 'text-[#00E5FF]'}`} />
|
||||
<span className="text-white/70 text-sm">
|
||||
{user?.hasFullBook ? "无限匹配机会" : matchesRemaining <= 0 ? "今日匹配机会已用完" : "剩余匹配机会"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-lg font-bold ${matchesRemaining > 0 ? 'text-[#00E5FF]' : 'text-red-400'}`}>
|
||||
{user?.hasFullBook ? "无限" : `${matchesRemaining}/${totalMatchesAllowed}`}
|
||||
</span>
|
||||
{matchesRemaining <= 0 && !user?.hasFullBook && (
|
||||
<button
|
||||
onClick={() => router.push('/chapters')}
|
||||
className="px-3 py-1.5 rounded-full bg-[#FFD700]/20 text-[#FFD700] text-xs font-medium"
|
||||
>
|
||||
购买小节+1次
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{!isMatching && !currentMatch && (
|
||||
<motion.div
|
||||
@@ -213,15 +387,17 @@ export default function MatchPage() {
|
||||
>
|
||||
{/* 中央匹配圆环 */}
|
||||
<motion.div
|
||||
onClick={startMatch}
|
||||
className="relative w-[280px] h-[280px] mb-8 cursor-pointer"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={hasPurchased ? startMatch : undefined}
|
||||
className={`relative w-[280px] h-[280px] mb-8 ${hasPurchased ? 'cursor-pointer' : 'cursor-not-allowed'}`}
|
||||
whileTap={hasPurchased ? { scale: 0.95 } : undefined}
|
||||
>
|
||||
{/* 外层光环 */}
|
||||
<motion.div
|
||||
className="absolute inset-[-30px] rounded-full"
|
||||
style={{
|
||||
background: "radial-gradient(circle, transparent 50%, rgba(0, 229, 255, 0.1) 70%, transparent 100%)",
|
||||
background: hasPurchased
|
||||
? "radial-gradient(circle, transparent 50%, rgba(0, 229, 255, 0.1) 70%, transparent 100%)"
|
||||
: "radial-gradient(circle, transparent 50%, rgba(100, 100, 100, 0.1) 70%, transparent 100%)",
|
||||
}}
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
@@ -236,7 +412,7 @@ export default function MatchPage() {
|
||||
|
||||
{/* 中间光环 */}
|
||||
<motion.div
|
||||
className="absolute inset-[-15px] rounded-full border-2 border-[#00E5FF]/30"
|
||||
className={`absolute inset-[-15px] rounded-full border-2 ${hasPurchased ? 'border-[#00E5FF]/30' : 'border-gray-600/30'}`}
|
||||
animate={{
|
||||
scale: [1, 1.05, 1],
|
||||
opacity: [0.3, 0.6, 0.3],
|
||||
@@ -252,8 +428,12 @@ export default function MatchPage() {
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full flex flex-col items-center justify-center overflow-hidden"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)",
|
||||
boxShadow: "0 0 60px rgba(0, 229, 255, 0.3), inset 0 0 60px rgba(123, 97, 255, 0.2)",
|
||||
background: hasPurchased
|
||||
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"
|
||||
: "linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 50%, #1a1a1a 100%)",
|
||||
boxShadow: hasPurchased
|
||||
? "0 0 60px rgba(0, 229, 255, 0.3), inset 0 0 60px rgba(123, 97, 255, 0.2)"
|
||||
: "0 0 30px rgba(100, 100, 100, 0.2)",
|
||||
}}
|
||||
animate={{
|
||||
y: [0, -5, 0],
|
||||
@@ -268,35 +448,70 @@ export default function MatchPage() {
|
||||
<div
|
||||
className="absolute inset-0 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle at 30% 30%, rgba(123, 97, 255, 0.4) 0%, transparent 50%), radial-gradient(circle at 70% 70%, rgba(233, 30, 99, 0.3) 0%, transparent 50%)",
|
||||
background: hasPurchased
|
||||
? "radial-gradient(circle at 30% 30%, rgba(123, 97, 255, 0.4) 0%, transparent 50%), radial-gradient(circle at 70% 70%, rgba(233, 30, 99, 0.3) 0%, transparent 50%)"
|
||||
: "radial-gradient(circle at 30% 30%, rgba(100, 100, 100, 0.2) 0%, transparent 50%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 中心图标 */}
|
||||
<Mic className="w-12 h-12 text-white/90 mb-3 relative z-10" />
|
||||
<div className="text-xl font-bold text-white mb-1 relative z-10">开始匹配</div>
|
||||
<div className="text-sm text-white/60 relative z-10">寻找{currentTypeLabel}</div>
|
||||
{hasPurchased ? (
|
||||
needPayToMatch ? (
|
||||
<>
|
||||
<Zap className="w-12 h-12 text-[#FFD700] mb-3 relative z-10" />
|
||||
<div className="text-xl font-bold text-white mb-1 relative z-10">需要解锁</div>
|
||||
<div className="text-sm text-white/60 relative z-10">今日免费次数已用完</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Users className="w-12 h-12 text-white/90 mb-3 relative z-10" />
|
||||
<div className="text-xl font-bold text-white mb-1 relative z-10">开始匹配</div>
|
||||
<div className="text-sm text-white/60 relative z-10">匹配{currentMatchLabel}</div>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Lock className="w-12 h-12 text-gray-500 mb-3 relative z-10" />
|
||||
<div className="text-xl font-bold text-gray-400 mb-1 relative z-10">购买后解锁</div>
|
||||
<div className="text-sm text-gray-500 relative z-10">购买9.9元即可使用</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* 当前模式显示 */}
|
||||
<p className="text-white/50 text-sm mb-8">
|
||||
当前模式: <span className="text-[#00E5FF]">{currentTypeLabel}</span>
|
||||
<p className="text-white/50 text-sm mb-4">
|
||||
当前模式: <span className={hasPurchased ? "text-[#00E5FF]" : "text-gray-500"}>{currentTypeLabel}</span>
|
||||
</p>
|
||||
|
||||
{/* 购买提示 */}
|
||||
{!hasPurchased && (
|
||||
<div className="w-full mb-6 p-4 rounded-xl bg-gradient-to-r from-[#00E5FF]/10 to-transparent border border-[#00E5FF]/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white font-medium">购买书籍解锁匹配功能</p>
|
||||
<p className="text-gray-400 text-sm mt-1">仅需9.9元,每天3次免费匹配</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/chapters')}
|
||||
className="px-4 py-2 rounded-lg bg-[#00E5FF] text-black text-sm font-medium"
|
||||
>
|
||||
去购买
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分隔线 */}
|
||||
<div className="w-full h-px bg-white/10 mb-6" />
|
||||
|
||||
{/* 选择匹配类型 - 修改为点击可加入的类型时弹出加入框 */}
|
||||
{/* 选择匹配类型 */}
|
||||
<p className="text-white/40 text-sm mb-4">选择匹配类型</p>
|
||||
<div className="grid grid-cols-4 gap-3 w-full">
|
||||
{matchTypes.map((type) => (
|
||||
<button
|
||||
key={type.id}
|
||||
onClick={() => {
|
||||
setSelectedType(type.id)
|
||||
}}
|
||||
onClick={() => setSelectedType(type.id)}
|
||||
className={`p-4 rounded-xl flex flex-col items-center gap-2 transition-all ${
|
||||
selectedType === type.id
|
||||
? "bg-[#00E5FF]/10 border border-[#00E5FF]/50"
|
||||
@@ -307,17 +522,6 @@ export default function MatchPage() {
|
||||
<span className={`text-xs ${selectedType === type.id ? "text-[#00E5FF]" : "text-white/60"}`}>
|
||||
{type.label}
|
||||
</span>
|
||||
{type.joinable && (
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleJoinClick(type.id)
|
||||
}}
|
||||
className="text-[10px] px-2 py-0.5 rounded-full bg-[#00E5FF]/20 text-[#00E5FF] mt-1 cursor-pointer hover:bg-[#00E5FF]/30"
|
||||
>
|
||||
加入
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -344,7 +548,7 @@ export default function MatchPage() {
|
||||
animate={{ scale: [1, 1.2, 1] }}
|
||||
transition={{ duration: 1, repeat: Number.POSITIVE_INFINITY }}
|
||||
>
|
||||
<Mic className="w-12 h-12 text-[#00E5FF]" />
|
||||
<Users className="w-12 h-12 text-[#00E5FF]" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -366,7 +570,7 @@ export default function MatchPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold mb-2 text-white">正在寻找合作伙伴...</h2>
|
||||
<h2 className="text-xl font-semibold mb-2 text-white">正在匹配{currentMatchLabel}...</h2>
|
||||
<p className="text-white/50 mb-8">已匹配 {matchAttempts} 次</p>
|
||||
|
||||
<button
|
||||
@@ -447,15 +651,79 @@ export default function MatchPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={nextMatch}
|
||||
className="w-full py-4 rounded-xl bg-[#1c1c1e] text-white border border-white/10"
|
||||
className="w-full py-4 rounded-xl bg-[#1c1c1e] text-white border border-white/10 flex items-center justify-center gap-2"
|
||||
>
|
||||
重新匹配
|
||||
{matchesRemaining <= 0 && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-[#FFD700]/20 text-[#FFD700]">需解锁</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 解锁匹配次数弹窗 */}
|
||||
<AnimatePresence>
|
||||
{showUnlockModal && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center px-6"
|
||||
onClick={() => !isUnlocking && setShowUnlockModal(false)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-[#1c1c1e] rounded-2xl w-full max-w-sm overflow-hidden"
|
||||
>
|
||||
<div className="p-6 text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[#FFD700]/20 flex items-center justify-center">
|
||||
<Zap className="w-8 h-8 text-[#FFD700]" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">匹配机会已用完</h3>
|
||||
<p className="text-white/60 text-sm mb-6">
|
||||
每购买一个小节内容即可额外获得1次匹配机会
|
||||
</p>
|
||||
|
||||
<div className="bg-black/30 rounded-xl p-4 mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-white/60">解锁方式</span>
|
||||
<span className="text-white font-medium">购买任意小节</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white/60">获得次数</span>
|
||||
<span className="text-[#00E5FF] font-medium">+1次匹配</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUnlockModal(false)
|
||||
router.push('/chapters')
|
||||
}}
|
||||
className="w-full py-3 rounded-xl bg-[#FFD700] text-black font-medium"
|
||||
>
|
||||
去购买小节 (¥1/节)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowUnlockModal(false)}
|
||||
className="w-full py-3 rounded-xl bg-white/5 text-white/60"
|
||||
>
|
||||
明天再来
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 加入类型弹窗 */}
|
||||
<AnimatePresence>
|
||||
{showJoinModal && (
|
||||
<motion.div
|
||||
@@ -486,30 +754,65 @@ export default function MatchPage() {
|
||||
{/* 弹窗内容 */}
|
||||
<div className="p-5">
|
||||
{joinSuccess ? (
|
||||
// 成功状态
|
||||
<motion.div initial={{ scale: 0.8 }} animate={{ scale: 1 }} className="text-center py-8">
|
||||
<CheckCircle className="w-16 h-16 text-[#00E5FF] mx-auto mb-4" />
|
||||
<p className="text-white text-lg font-medium mb-2">加入成功!</p>
|
||||
<p className="text-white/60 text-sm">我们会尽快与您联系</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
// 表单状态
|
||||
<>
|
||||
<p className="text-white/60 text-sm mb-4">
|
||||
{getStoredPhone() ? "检测到您已注册的手机号,确认后即可加入" : "请输入您的手机号以便我们联系您"}
|
||||
{user?.phone ? "已检测到您的绑定信息,可直接提交或修改" : "请填写您的联系方式以便我们联系您"}
|
||||
</p>
|
||||
|
||||
{/* 手机号输入 */}
|
||||
{/* 联系方式类型切换 */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setContactType("phone")}
|
||||
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${
|
||||
contactType === "phone"
|
||||
? "bg-[#00E5FF]/20 text-[#00E5FF] border border-[#00E5FF]/30"
|
||||
: "bg-white/5 text-gray-400 border border-white/10"
|
||||
}`}
|
||||
>
|
||||
手机号
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setContactType("wechat")}
|
||||
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${
|
||||
contactType === "wechat"
|
||||
? "bg-[#07C160]/20 text-[#07C160] border border-[#07C160]/30"
|
||||
: "bg-white/5 text-gray-400 border border-white/10"
|
||||
}`}
|
||||
>
|
||||
微信号
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 联系方式输入 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-white/40 text-xs mb-2">手机号</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value.replace(/\D/g, "").slice(0, 11))}
|
||||
placeholder="请输入11位手机号"
|
||||
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#00E5FF]/50"
|
||||
disabled={isJoining}
|
||||
/>
|
||||
<label className="block text-white/40 text-xs mb-2">
|
||||
{contactType === "phone" ? "手机号" : "微信号"}
|
||||
</label>
|
||||
{contactType === "phone" ? (
|
||||
<input
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value.replace(/\D/g, "").slice(0, 11))}
|
||||
placeholder="请输入11位手机号"
|
||||
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#00E5FF]/50"
|
||||
disabled={isJoining}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={wechatId}
|
||||
onChange={(e) => setWechatId(e.target.value)}
|
||||
placeholder="请输入微信号"
|
||||
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#07C160]/50"
|
||||
disabled={isJoining}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
@@ -518,7 +821,7 @@ export default function MatchPage() {
|
||||
{/* 提交按钮 */}
|
||||
<button
|
||||
onClick={handleJoinSubmit}
|
||||
disabled={isJoining || !phoneNumber}
|
||||
disabled={isJoining || (contactType === "phone" ? !phoneNumber : !wechatId)}
|
||||
className="w-full py-3 rounded-xl bg-[#00E5FF] text-black font-medium flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isJoining ? (
|
||||
@@ -551,25 +854,12 @@ export default function MatchPage() {
|
||||
<List className="w-5 h-5 text-gray-500 mb-1" />
|
||||
<span className="text-gray-500 text-xs">目录</span>
|
||||
</button>
|
||||
{/* 匹配按钮 - 当前页面高亮,小星球图标 */}
|
||||
{/* 找伙伴按钮 - 当前页面高亮 */}
|
||||
<button className="flex flex-col items-center py-2 px-6 -mt-4">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
|
||||
<svg className="w-7 h-7 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="8" fill="currentColor" opacity="0.9" />
|
||||
<ellipse
|
||||
cx="12"
|
||||
cy="12"
|
||||
rx="11"
|
||||
ry="4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
opacity="0.6"
|
||||
/>
|
||||
<circle cx="9" cy="10" r="1.5" fill="white" opacity="0.4" />
|
||||
</svg>
|
||||
<Users className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className="text-[#00CED1] text-xs font-medium mt-1">匹配</span>
|
||||
<span className="text-[#00CED1] text-xs font-medium mt-1">找伙伴</span>
|
||||
</button>
|
||||
<button onClick={() => router.push("/my")} className="flex flex-col items-center py-2 px-4">
|
||||
<User className="w-5 h-5 text-gray-500 mb-1" />
|
||||
|
||||
596
app/my/page.tsx
596
app/my/page.tsx
@@ -2,27 +2,24 @@
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { User, ChevronRight, Copy, Check, Home, List, TrendingUp, Gift, Star, Info } from "lucide-react"
|
||||
import { User, ChevronRight, Home, List, Gift, Star, Info, Users, Wallet, Footprints, Eye, BookOpen, Clock, ArrowUpRight, Phone, MessageCircle, CreditCard, X, Check, Loader2, Settings } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { AuthModal } from "@/components/modules/auth/auth-modal"
|
||||
import { getFullBookPrice, getTotalSectionCount } from "@/lib/book-data"
|
||||
|
||||
function PlanetIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="8" fill="currentColor" opacity="0.9" />
|
||||
<ellipse cx="12" cy="12" rx="11" ry="4" fill="none" stroke="currentColor" strokeWidth="1.5" opacity="0.6" />
|
||||
<circle cx="9" cy="10" r="1.5" fill="white" opacity="0.4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MyPage() {
|
||||
const router = useRouter()
|
||||
const { user, isLoggedIn, logout, getAllPurchases, settings } = useStore()
|
||||
const { user, isLoggedIn, logout, getAllPurchases, settings, updateUser } = useStore()
|
||||
const [showAuthModal, setShowAuthModal] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<"overview" | "footprint">("overview")
|
||||
|
||||
// 绑定弹窗状态
|
||||
const [showBindModal, setShowBindModal] = useState(false)
|
||||
const [bindType, setBindType] = useState<"phone" | "wechat" | "alipay">("phone")
|
||||
const [bindValue, setBindValue] = useState("")
|
||||
const [isBinding, setIsBinding] = useState(false)
|
||||
const [bindError, setBindError] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
@@ -36,25 +33,63 @@ export default function MyPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const fullBookPrice = getFullBookPrice()
|
||||
const totalSections = getTotalSectionCount()
|
||||
const purchasedCount = user?.hasFullBook ? totalSections : user?.purchasedSections?.length || 0
|
||||
const readingMinutes = Math.floor(Math.random() * 100)
|
||||
const bookmarks = user?.purchasedSections?.length || 0
|
||||
|
||||
const authorInfo = settings?.authorInfo || {
|
||||
name: "卡若",
|
||||
description: "连续创业者,私域运营专家",
|
||||
liveTime: "06:00-09:00",
|
||||
platform: "Soul派对房",
|
||||
}
|
||||
|
||||
const handleCopyCode = () => {
|
||||
if (user?.referralCode) {
|
||||
navigator.clipboard.writeText(user.referralCode)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
// 绑定账号
|
||||
const handleBind = async () => {
|
||||
if (!bindValue.trim()) {
|
||||
setBindError("请输入内容")
|
||||
return
|
||||
}
|
||||
|
||||
if (bindType === "phone" && !/^1[3-9]\d{9}$/.test(bindValue)) {
|
||||
setBindError("请输入正确的手机号")
|
||||
return
|
||||
}
|
||||
|
||||
if (bindType === "wechat" && bindValue.length < 6) {
|
||||
setBindError("微信号至少6位")
|
||||
return
|
||||
}
|
||||
|
||||
if (bindType === "alipay" && !bindValue.includes("@") && !/^1[3-9]\d{9}$/.test(bindValue)) {
|
||||
setBindError("请输入正确的支付宝账号")
|
||||
return
|
||||
}
|
||||
|
||||
setIsBinding(true)
|
||||
setBindError("")
|
||||
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 更新用户信息
|
||||
if (updateUser && user) {
|
||||
const updates: any = {}
|
||||
if (bindType === "phone") updates.phone = bindValue
|
||||
if (bindType === "wechat") updates.wechat = bindValue
|
||||
if (bindType === "alipay") updates.alipay = bindValue
|
||||
updateUser(user.id, updates)
|
||||
}
|
||||
|
||||
setShowBindModal(false)
|
||||
setBindValue("")
|
||||
alert("绑定成功!")
|
||||
} catch (error) {
|
||||
setBindError("绑定失败,请重试")
|
||||
} finally {
|
||||
setIsBinding(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开绑定弹窗
|
||||
const openBindModal = (type: "phone" | "wechat" | "alipay") => {
|
||||
setBindType(type)
|
||||
setBindValue("")
|
||||
setBindError("")
|
||||
setShowBindModal(true)
|
||||
}
|
||||
|
||||
// 底部导航组件
|
||||
@@ -70,12 +105,12 @@ export default function MyPage() {
|
||||
<List className="w-5 h-5 text-gray-500 mb-1" />
|
||||
<span className="text-gray-500 text-xs">目录</span>
|
||||
</button>
|
||||
{/* 匹配按钮 - 小星球图标 */}
|
||||
{/* 找伙伴按钮 */}
|
||||
<button onClick={() => router.push("/match")} className="flex flex-col items-center py-2 px-6 -mt-4">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
|
||||
<PlanetIcon className="w-7 h-7 text-white" />
|
||||
<Users className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className="text-gray-500 text-xs mt-1">匹配</span>
|
||||
<span className="text-gray-500 text-xs mt-1">找伙伴</span>
|
||||
</button>
|
||||
<button className="flex flex-col items-center py-2 px-4">
|
||||
<User className="w-5 h-5 text-[#00CED1] mb-1" />
|
||||
@@ -94,7 +129,7 @@ export default function MyPage() {
|
||||
<h1 className="text-lg font-medium text-[#00CED1]">我的</h1>
|
||||
</div>
|
||||
|
||||
{/* 用户卡片 - 突出个性化 */}
|
||||
{/* 用户卡片 */}
|
||||
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-br from-[#1c1c1e] to-[#2c2c2e] border border-[#00CED1]/20">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-16 h-16 rounded-full border-2 border-dashed border-[#00CED1]/50 flex items-center justify-center bg-gradient-to-br from-[#00CED1]/10 to-transparent">
|
||||
@@ -106,99 +141,42 @@ export default function MyPage() {
|
||||
</button>
|
||||
<p className="text-white/30 text-sm">解锁专属权益</p>
|
||||
</div>
|
||||
<div className="px-3 py-1 rounded-full bg-[#00CED1]/20 border border-[#00CED1]/30">
|
||||
<span className="text-[#00CED1] text-xs">VIP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 个性化数据 */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-4 border-t border-white/10">
|
||||
<div className="text-center p-2 rounded-lg bg-white/5">
|
||||
<p className="text-[#00CED1] text-xl font-bold">0</p>
|
||||
<p className="text-white/40 text-xs">已读</p>
|
||||
<p className="text-white/40 text-xs">已购章节</p>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-white/5">
|
||||
<p className="text-[#00CED1] text-xl font-bold">0</p>
|
||||
<p className="text-white/40 text-xs">收藏</p>
|
||||
<p className="text-white/40 text-xs">推荐好友</p>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-white/5">
|
||||
<p className="text-[#00CED1] text-xl font-bold">0</p>
|
||||
<p className="text-white/40 text-xs">书签</p>
|
||||
<p className="text-[#FFD700] text-xl font-bold">--</p>
|
||||
<p className="text-white/40 text-xs">待领收益</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 收益中心 - 突出收益 */}
|
||||
{/* 分销入口 */}
|
||||
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-r from-[#FFD700]/10 via-[#1c1c1e] to-[#1c1c1e] border border-[#FFD700]/20">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#FFD700]/20 flex items-center justify-center">
|
||||
<TrendingUp className="w-4 h-4 text-[#FFD700]" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#FFD700] to-[#FFA500] flex items-center justify-center">
|
||||
<Gift className="w-5 h-5 text-black" />
|
||||
</div>
|
||||
<span className="text-white font-medium">收益中心</span>
|
||||
</div>
|
||||
<span className="text-[#FFD700] text-sm bg-[#FFD700]/10 px-3 py-1 rounded-full">90%分成</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-[#FFD700]/5 to-transparent rounded-xl p-4 mb-4">
|
||||
<p className="text-white/50 text-sm text-center mb-1">累计收益</p>
|
||||
<p className="text-[#FFD700] text-3xl font-bold text-center">¥0.00</p>
|
||||
<div className="flex justify-center gap-8 mt-3">
|
||||
<div className="text-center">
|
||||
<p className="text-white/40 text-xs">可提现</p>
|
||||
<p className="text-white font-medium">¥0.00</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white/40 text-xs">已提现</p>
|
||||
<p className="text-white font-medium">¥0.00</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<button
|
||||
onClick={() => setShowAuthModal(true)}
|
||||
className="py-3 rounded-xl bg-gradient-to-r from-[#FFD700] to-[#FFA500] text-black font-medium flex items-center justify-center gap-2"
|
||||
>
|
||||
<Gift className="w-4 h-4" />
|
||||
生成海报
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAuthModal(true)}
|
||||
className="py-3 rounded-xl bg-[#2c2c2e] text-white font-medium border border-white/10"
|
||||
>
|
||||
立即提现
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 pt-4 border-t border-white/10">
|
||||
<div className="text-center">
|
||||
<p className="text-[#FFD700] text-xl font-bold">0</p>
|
||||
<p className="text-white/40 text-xs">推荐人数</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[#FFD700] text-xl font-bold">0</p>
|
||||
<p className="text-white/40 text-xs">成交订单</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[#FFD700] text-xl font-bold">90%</p>
|
||||
<p className="text-white/40 text-xs">佣金率</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/50 text-xs">我的邀请码</p>
|
||||
<p className="text-[#FFD700] font-mono text-lg">- - -</p>
|
||||
<p className="text-white font-medium">推广赚收益</p>
|
||||
<p className="text-white/40 text-xs">登录后查看详情</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAuthModal(true)}
|
||||
className="px-4 py-2 rounded-lg bg-[#2c2c2e] text-white text-sm"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAuthModal(true)}
|
||||
className="px-4 py-2 rounded-lg bg-[#FFD700]/20 text-[#FFD700] text-sm font-medium"
|
||||
>
|
||||
立即登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -214,17 +192,6 @@ export default function MyPage() {
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAuthModal(true)}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">🏷️</span>
|
||||
<span className="text-white">我的书签</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</button>
|
||||
{/* 关于作者 - 小图标入口 */}
|
||||
<button
|
||||
onClick={() => router.push("/about")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
@@ -248,6 +215,13 @@ export default function MyPage() {
|
||||
// 已登录状态
|
||||
const userPurchases = getAllPurchases().filter((p) => p.userId === user?.id)
|
||||
const completedOrders = userPurchases.filter((p) => p.status === "completed").length
|
||||
|
||||
// 模拟足迹数据(实际应从数据库获取)
|
||||
const footprintData = {
|
||||
recentChapters: user?.purchasedSections?.slice(-5) || [],
|
||||
matchHistory: [], // 匹配历史
|
||||
totalReadTime: Math.floor(Math.random() * 200) + 50, // 阅读时长(分钟)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-white pb-24">
|
||||
@@ -255,7 +229,7 @@ export default function MyPage() {
|
||||
<h1 className="text-lg font-medium text-[#00CED1]">我的</h1>
|
||||
</div>
|
||||
|
||||
{/* 用户卡片 - 突出个性化 */}
|
||||
{/* 用户卡片 */}
|
||||
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-br from-[#1c1c1e] to-[#2c2c2e] border border-[#00CED1]/20">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-16 h-16 rounded-full border-2 border-[#00CED1] flex items-center justify-center bg-gradient-to-br from-[#00CED1]/20 to-transparent">
|
||||
@@ -268,150 +242,314 @@ export default function MyPage() {
|
||||
<div className="px-3 py-1 rounded-full bg-[#00CED1]/20 border border-[#00CED1]/30">
|
||||
<span className="text-[#00CED1] text-xs flex items-center gap-1">
|
||||
<Star className="w-3 h-3" />
|
||||
VIP
|
||||
创业伙伴
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 个性化数据 */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-4 border-t border-white/10">
|
||||
<div className="text-center p-2 rounded-lg bg-white/5">
|
||||
<p className="text-[#00CED1] text-xl font-bold">{purchasedCount}</p>
|
||||
<p className="text-white/40 text-xs">已读</p>
|
||||
<p className="text-white/40 text-xs">已购章节</p>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-white/5">
|
||||
<p className="text-[#00CED1] text-xl font-bold">{readingMinutes}</p>
|
||||
<p className="text-white/40 text-xs">时长(分)</p>
|
||||
<p className="text-[#00CED1] text-xl font-bold">{user?.referralCount || 0}</p>
|
||||
<p className="text-white/40 text-xs">推荐好友</p>
|
||||
</div>
|
||||
<div className="text-center p-2 rounded-lg bg-white/5">
|
||||
<p className="text-[#00CED1] text-xl font-bold">{bookmarks}</p>
|
||||
<p className="text-white/40 text-xs">书签</p>
|
||||
<p className="text-[#FFD700] text-xl font-bold">
|
||||
{(user?.earnings || 0) > 0 ? `¥${(user?.earnings || 0).toFixed(0)}` : '--'}
|
||||
</p>
|
||||
<p className="text-white/40 text-xs">待领收益</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 收益中心 - 突出收益 */}
|
||||
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-r from-[#FFD700]/10 via-[#1c1c1e] to-[#1c1c1e] border border-[#FFD700]/20">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#FFD700]/20 flex items-center justify-center">
|
||||
<TrendingUp className="w-4 h-4 text-[#FFD700]" />
|
||||
</div>
|
||||
<span className="text-white font-medium">收益中心</span>
|
||||
</div>
|
||||
<span className="text-[#FFD700] text-sm bg-[#FFD700]/10 px-3 py-1 rounded-full">90%分成</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-[#FFD700]/5 to-transparent rounded-xl p-4 mb-4">
|
||||
<p className="text-white/50 text-sm text-center mb-1">累计收益</p>
|
||||
<p className="text-[#FFD700] text-3xl font-bold text-center">¥{(user?.earnings || 0).toFixed(2)}</p>
|
||||
<div className="flex justify-center gap-8 mt-3">
|
||||
<div className="text-center">
|
||||
<p className="text-white/40 text-xs">可提现</p>
|
||||
<p className="text-white font-medium">¥{(user?.pendingEarnings || 0).toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white/40 text-xs">已提现</p>
|
||||
<p className="text-white font-medium">¥{(user?.withdrawnEarnings || 0).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<button
|
||||
onClick={() => router.push("/my/referral")}
|
||||
className="py-3 rounded-xl bg-gradient-to-r from-[#FFD700] to-[#FFA500] text-black font-medium flex items-center justify-center gap-2"
|
||||
>
|
||||
<Gift className="w-4 h-4" />
|
||||
生成海报
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/my/referral")}
|
||||
className="py-3 rounded-xl bg-[#2c2c2e] text-white font-medium border border-white/10"
|
||||
>
|
||||
立即提现
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 pt-4 border-t border-white/10">
|
||||
<div className="text-center">
|
||||
<p className="text-[#FFD700] text-xl font-bold">{user?.referralCount || 0}</p>
|
||||
<p className="text-white/40 text-xs">推荐人数</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[#FFD700] text-xl font-bold">{completedOrders}</p>
|
||||
<p className="text-white/40 text-xs">成交订单</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[#FFD700] text-xl font-bold">90%</p>
|
||||
<p className="text-white/40 text-xs">佣金率</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white/50 text-xs">我的邀请码</p>
|
||||
<p className="text-[#FFD700] font-mono text-lg">{user?.referralCode || "---"}</p>
|
||||
{/* 收益卡片 - 艺术化设计 */}
|
||||
<div className="mx-4 mt-4 p-4 rounded-2xl bg-gradient-to-br from-[#1a1a2e] via-[#16213e] to-[#0f3460] border border-[#00CED1]/20 relative overflow-hidden">
|
||||
{/* 背景装饰 */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-[#FFD700]/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-24 h-24 bg-gradient-to-tr from-[#00CED1]/10 to-transparent rounded-full translate-y-1/2 -translate-x-1/2" />
|
||||
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="w-5 h-5 text-[#FFD700]" />
|
||||
<span className="text-white font-medium">我的收益</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCopyCode}
|
||||
className="px-4 py-2 rounded-lg bg-[#2c2c2e] text-white text-sm flex items-center gap-2"
|
||||
onClick={() => router.push("/my/referral")}
|
||||
className="text-[#00CED1] text-xs flex items-center gap-1"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-[#00CED1]" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? "已复制" : "复制"}
|
||||
推广中心
|
||||
<ArrowUpRight className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-6 mb-4">
|
||||
<div>
|
||||
<p className="text-white/50 text-xs mb-1">累计收益</p>
|
||||
<p className="text-3xl font-bold bg-gradient-to-r from-[#FFD700] to-[#FFA500] bg-clip-text text-transparent">
|
||||
¥{(user?.earnings || 0).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white/50 text-xs mb-1">可提现</p>
|
||||
<p className="text-xl font-semibold text-white">
|
||||
¥{(user?.pendingEarnings || 0).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => router.push("/my/referral")}
|
||||
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-[#FFD700]/80 to-[#FFA500]/80 text-black text-sm font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
<Gift className="w-4 h-4" />
|
||||
推广中心 / 提现
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 菜单列表 */}
|
||||
<div className="mx-4 mt-4 rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
|
||||
{/* Tab切换 */}
|
||||
<div className="mx-4 mt-4 flex gap-2">
|
||||
<button
|
||||
onClick={() => router.push("/my/purchases")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
onClick={() => setActiveTab("overview")}
|
||||
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-colors ${
|
||||
activeTab === "overview"
|
||||
? "bg-[#00CED1]/20 text-[#00CED1] border border-[#00CED1]/30"
|
||||
: "bg-[#1c1c1e] text-white/60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">📦</span>
|
||||
<span className="text-white">我的订单</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
概览
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/my/bookmarks")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
onClick={() => setActiveTab("footprint")}
|
||||
className={`flex-1 py-2.5 rounded-xl text-sm font-medium transition-colors flex items-center justify-center gap-1 ${
|
||||
activeTab === "footprint"
|
||||
? "bg-[#00CED1]/20 text-[#00CED1] border border-[#00CED1]/30"
|
||||
: "bg-[#1c1c1e] text-white/60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">🏷️</span>
|
||||
<span className="text-white">我的书签</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</button>
|
||||
{/* 关于作者 - 小图标入口 */}
|
||||
<button
|
||||
onClick={() => router.push("/about")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center">
|
||||
<Info className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="text-white">关于作者</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
<Footprints className="w-4 h-4" />
|
||||
我的足迹
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mx-4 mt-4">
|
||||
<button
|
||||
onClick={logout}
|
||||
className="w-full py-3 rounded-xl bg-[#1c1c1e] text-[#00CED1] font-medium border border-[#00CED1]/30"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
{activeTab === "overview" ? (
|
||||
<>
|
||||
{/* 菜单列表 */}
|
||||
<div className="mx-4 mt-4 rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
|
||||
<button
|
||||
onClick={() => router.push("/my/purchases")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">📦</span>
|
||||
<span className="text-white">我的订单</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white/40 text-sm">{completedOrders}笔</span>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/my/referral")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#FFD700] to-[#FFA500] flex items-center justify-center">
|
||||
<Gift className="w-3 h-3 text-black" />
|
||||
</div>
|
||||
<span className="text-white">推广中心</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[#FFD700] text-sm font-medium">90%佣金</span>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/about")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center">
|
||||
<Info className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="text-white">关于作者</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/my/settings")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 rounded-full bg-gray-500/20 flex items-center justify-center">
|
||||
<Settings className="w-3 h-3 text-gray-400" />
|
||||
</div>
|
||||
<span className="text-white">设置</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 足迹内容 */}
|
||||
<div className="mx-4 mt-4 space-y-4">
|
||||
{/* 阅读统计 */}
|
||||
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
|
||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||
<Eye className="w-4 h-4 text-[#00CED1]" />
|
||||
阅读统计
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="text-center p-3 rounded-xl bg-white/5">
|
||||
<BookOpen className="w-5 h-5 text-[#00CED1] mx-auto mb-1" />
|
||||
<p className="text-white font-bold">{purchasedCount}</p>
|
||||
<p className="text-white/40 text-xs">已读章节</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-xl bg-white/5">
|
||||
<Clock className="w-5 h-5 text-[#FFD700] mx-auto mb-1" />
|
||||
<p className="text-white font-bold">{footprintData.totalReadTime}</p>
|
||||
<p className="text-white/40 text-xs">阅读分钟</p>
|
||||
</div>
|
||||
<div className="text-center p-3 rounded-xl bg-white/5">
|
||||
<Users className="w-5 h-5 text-[#E91E63] mx-auto mb-1" />
|
||||
<p className="text-white font-bold">{footprintData.matchHistory.length || 0}</p>
|
||||
<p className="text-white/40 text-xs">匹配伙伴</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最近阅读 */}
|
||||
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
|
||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-[#00CED1]" />
|
||||
最近阅读
|
||||
</h3>
|
||||
{footprintData.recentChapters.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{footprintData.recentChapters.map((sectionId, index) => (
|
||||
<div
|
||||
key={sectionId}
|
||||
className="flex items-center justify-between p-3 rounded-xl bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-white/30 text-sm">{index + 1}</span>
|
||||
<span className="text-white text-sm">章节 {sectionId}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push(`/read/${sectionId}`)}
|
||||
className="text-[#00CED1] text-xs"
|
||||
>
|
||||
继续阅读
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-white/40">
|
||||
<BookOpen className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无阅读记录</p>
|
||||
<button
|
||||
onClick={() => router.push("/chapters")}
|
||||
className="mt-2 text-[#00CED1] text-sm"
|
||||
>
|
||||
去阅读 →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 匹配记录 */}
|
||||
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
|
||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-[#00CED1]" />
|
||||
匹配记录
|
||||
</h3>
|
||||
<div className="text-center py-6 text-white/40">
|
||||
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无匹配记录</p>
|
||||
<button
|
||||
onClick={() => router.push("/match")}
|
||||
className="mt-2 text-[#00CED1] text-sm"
|
||||
>
|
||||
去匹配 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<BottomNavBar />
|
||||
|
||||
{/* 绑定弹窗 */}
|
||||
{showBindModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => !isBinding && setShowBindModal(false)} />
|
||||
<div className="relative w-full max-w-sm bg-[#1c1c1e] rounded-2xl overflow-hidden">
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
绑定{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝"}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => !isBinding && setShowBindModal(false)}
|
||||
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
|
||||
>
|
||||
<X className="w-4 h-4 text-white/60" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-5">
|
||||
<div className="mb-4">
|
||||
<label className="block text-white/40 text-xs mb-2">
|
||||
{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝账号"}
|
||||
</label>
|
||||
<input
|
||||
type={bindType === "phone" ? "tel" : "text"}
|
||||
value={bindValue}
|
||||
onChange={(e) => setBindValue(e.target.value)}
|
||||
placeholder={
|
||||
bindType === "phone" ? "请输入11位手机号" :
|
||||
bindType === "wechat" ? "请输入微信号" :
|
||||
"请输入支付宝账号"
|
||||
}
|
||||
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#00CED1]/50"
|
||||
disabled={isBinding}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bindError && (
|
||||
<p className="text-red-400 text-sm mb-4">{bindError}</p>
|
||||
)}
|
||||
|
||||
<p className="text-white/40 text-xs mb-4">
|
||||
{bindType === "phone" && "绑定手机号后可用于找伙伴匹配"}
|
||||
{bindType === "wechat" && "绑定微信号后可用于找伙伴匹配和好友添加"}
|
||||
{bindType === "alipay" && "绑定支付宝后可用于提现收益"}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleBind}
|
||||
disabled={isBinding || !bindValue}
|
||||
className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{isBinding ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
绑定中...
|
||||
</>
|
||||
) : (
|
||||
"确认绑定"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
259
app/my/settings/page.tsx
Normal file
259
app/my/settings/page.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { ChevronLeft, Phone, MessageCircle, CreditCard, Check, X, Loader2, Shield } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter()
|
||||
const { user, updateUser, logout } = useStore()
|
||||
|
||||
// 绑定弹窗状态
|
||||
const [showBindModal, setShowBindModal] = useState(false)
|
||||
const [bindType, setBindType] = useState<"phone" | "wechat" | "alipay">("phone")
|
||||
const [bindValue, setBindValue] = useState("")
|
||||
const [isBinding, setIsBinding] = useState(false)
|
||||
const [bindError, setBindError] = useState("")
|
||||
|
||||
// 绑定账号
|
||||
const handleBind = async () => {
|
||||
if (!bindValue.trim()) {
|
||||
setBindError("请输入内容")
|
||||
return
|
||||
}
|
||||
|
||||
if (bindType === "phone" && !/^1[3-9]\d{9}$/.test(bindValue)) {
|
||||
setBindError("请输入正确的手机号")
|
||||
return
|
||||
}
|
||||
|
||||
if (bindType === "wechat" && bindValue.length < 6) {
|
||||
setBindError("微信号至少6位")
|
||||
return
|
||||
}
|
||||
|
||||
if (bindType === "alipay" && !bindValue.includes("@") && !/^1[3-9]\d{9}$/.test(bindValue)) {
|
||||
setBindError("请输入正确的支付宝账号")
|
||||
return
|
||||
}
|
||||
|
||||
setIsBinding(true)
|
||||
setBindError("")
|
||||
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (updateUser && user) {
|
||||
const updates: any = {}
|
||||
if (bindType === "phone") updates.phone = bindValue
|
||||
if (bindType === "wechat") updates.wechat = bindValue
|
||||
if (bindType === "alipay") updates.alipay = bindValue
|
||||
updateUser(user.id, updates)
|
||||
}
|
||||
|
||||
setShowBindModal(false)
|
||||
setBindValue("")
|
||||
alert("绑定成功!")
|
||||
} catch (error) {
|
||||
setBindError("绑定失败,请重试")
|
||||
} finally {
|
||||
setIsBinding(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openBindModal = (type: "phone" | "wechat" | "alipay") => {
|
||||
setBindType(type)
|
||||
setBindValue("")
|
||||
setBindError("")
|
||||
setShowBindModal(true)
|
||||
}
|
||||
|
||||
// 检查是否有绑定任何支付方式
|
||||
const hasAnyPaymentBound = user?.wechat || user?.alipay
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white pb-24">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
|
||||
<div className="px-4 py-3 flex items-center">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
<h1 className="flex-1 text-center text-lg font-semibold text-white">设置</h1>
|
||||
<div className="w-8" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="px-4 py-4 space-y-4">
|
||||
{/* 账号绑定 */}
|
||||
<div className="rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-white/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-[#00CED1]" />
|
||||
<span className="text-white font-medium">账号绑定</span>
|
||||
</div>
|
||||
<p className="text-white/40 text-xs mt-1">绑定后可用于提现和找伙伴功能</p>
|
||||
</div>
|
||||
|
||||
{/* 手机号 */}
|
||||
<button
|
||||
onClick={() => openBindModal("phone")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
user?.phone ? "bg-[#00CED1]/20" : "bg-white/10"
|
||||
}`}>
|
||||
<Phone className={`w-4 h-4 ${user?.phone ? "text-[#00CED1]" : "text-white/40"}`} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-white text-sm">手机号</p>
|
||||
<p className="text-white/40 text-xs">
|
||||
{user?.phone || "未绑定"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{user?.phone ? (
|
||||
<Check className="w-5 h-5 text-[#00CED1]" />
|
||||
) : (
|
||||
<span className="text-[#00CED1] text-xs">去绑定</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 微信号 */}
|
||||
<button
|
||||
onClick={() => openBindModal("wechat")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
user?.wechat ? "bg-[#07C160]/20" : "bg-white/10"
|
||||
}`}>
|
||||
<MessageCircle className={`w-4 h-4 ${user?.wechat ? "text-[#07C160]" : "text-white/40"}`} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-white text-sm">微信号</p>
|
||||
<p className="text-white/40 text-xs">
|
||||
{user?.wechat || "未绑定"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{user?.wechat ? (
|
||||
<Check className="w-5 h-5 text-[#07C160]" />
|
||||
) : (
|
||||
<span className="text-[#07C160] text-xs">去绑定</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 支付宝 */}
|
||||
<button
|
||||
onClick={() => openBindModal("alipay")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
user?.alipay ? "bg-[#1677FF]/20" : "bg-white/10"
|
||||
}`}>
|
||||
<CreditCard className={`w-4 h-4 ${user?.alipay ? "text-[#1677FF]" : "text-white/40"}`} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-white text-sm">支付宝</p>
|
||||
<p className="text-white/40 text-xs">
|
||||
{user?.alipay || "未绑定"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{user?.alipay ? (
|
||||
<Check className="w-5 h-5 text-[#1677FF]" />
|
||||
) : (
|
||||
<span className="text-[#1677FF] text-xs">去绑定</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 绑定提示 */}
|
||||
{!hasAnyPaymentBound && (
|
||||
<div className="p-4 rounded-xl bg-orange-500/10 border border-orange-500/20">
|
||||
<p className="text-orange-400 text-xs">
|
||||
提示:绑定至少一个支付方式(微信或支付宝)才能使用提现功能
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 退出登录 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
logout()
|
||||
router.push("/")
|
||||
}}
|
||||
className="w-full py-3 rounded-xl bg-[#1c1c1e] text-red-400 font-medium border border-red-400/30"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</main>
|
||||
|
||||
{/* 绑定弹窗 */}
|
||||
{showBindModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => !isBinding && setShowBindModal(false)} />
|
||||
<div className="relative w-full max-w-sm bg-[#1c1c1e] rounded-2xl overflow-hidden">
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
绑定{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝"}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => !isBinding && setShowBindModal(false)}
|
||||
className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center"
|
||||
>
|
||||
<X className="w-4 h-4 text-white/60" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-5">
|
||||
<div className="mb-4">
|
||||
<label className="block text-white/40 text-xs mb-2">
|
||||
{bindType === "phone" ? "手机号" : bindType === "wechat" ? "微信号" : "支付宝账号"}
|
||||
</label>
|
||||
<input
|
||||
type={bindType === "phone" ? "tel" : "text"}
|
||||
value={bindValue}
|
||||
onChange={(e) => setBindValue(e.target.value)}
|
||||
placeholder={
|
||||
bindType === "phone" ? "请输入11位手机号" :
|
||||
bindType === "wechat" ? "请输入微信号" :
|
||||
"请输入支付宝账号"
|
||||
}
|
||||
className="w-full px-4 py-3 rounded-xl bg-black/30 border border-white/10 text-white placeholder-white/30 focus:outline-none focus:border-[#00CED1]/50"
|
||||
disabled={isBinding}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{bindError && (
|
||||
<p className="text-red-400 text-sm mb-4">{bindError}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleBind}
|
||||
disabled={isBinding || !bindValue}
|
||||
className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{isBinding ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
绑定中...
|
||||
</>
|
||||
) : (
|
||||
"确认绑定"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
app/page.tsx
40
app/page.tsx
@@ -1,8 +1,13 @@
|
||||
/**
|
||||
* 一场SOUL的创业实验 - 首页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Search, ChevronRight, BookOpen, Home, List, User } from "lucide-react"
|
||||
import { Search, ChevronRight, BookOpen, Home, List, User, Users } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { bookData, getTotalSectionCount } from "@/lib/book-data"
|
||||
|
||||
@@ -35,11 +40,7 @@ export default function HomePage() {
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00CED1]" />
|
||||
</div>
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -48,12 +49,12 @@ export default function HomePage() {
|
||||
<header className="px-4 pt-6 pb-4">
|
||||
<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-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-white" />
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
|
||||
<span className="text-white font-bold text-lg">S</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">创业实验</h1>
|
||||
<p className="text-xs text-gray-500">真实的商业世界</p>
|
||||
<h1 className="text-lg font-bold text-white">Soul<span className="text-[#00CED1]">创业实验</span></h1>
|
||||
<p className="text-xs text-gray-500">来自派对房的真实故事</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -220,25 +221,12 @@ export default function HomePage() {
|
||||
<List className="w-5 h-5 text-gray-500 mb-1" />
|
||||
<span className="text-gray-500 text-xs">目录</span>
|
||||
</button>
|
||||
{/* 匹配按钮 - 小星球图标 */}
|
||||
{/* 找伙伴按钮 */}
|
||||
<button onClick={() => router.push("/match")} className="flex flex-col items-center py-2 px-6 -mt-4">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
|
||||
<svg className="w-7 h-7 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<circle cx="12" cy="12" r="8" fill="currentColor" opacity="0.9" />
|
||||
<ellipse
|
||||
cx="12"
|
||||
cy="12"
|
||||
rx="11"
|
||||
ry="4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
opacity="0.6"
|
||||
/>
|
||||
<circle cx="9" cy="10" r="1.5" fill="white" opacity="0.4" />
|
||||
</svg>
|
||||
<Users className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className="text-[#00CED1] text-xs mt-1">匹配</span>
|
||||
<span className="text-gray-500 text-xs mt-1">找伙伴</span>
|
||||
</button>
|
||||
<button onClick={() => router.push("/my")} className="flex flex-col items-center py-2 px-4">
|
||||
<User className="w-5 h-5 text-gray-500 mb-1" />
|
||||
|
||||
@@ -46,7 +46,7 @@ export default async function ReadPage({ params }: ReadPageProps) {
|
||||
|
||||
notFound()
|
||||
} catch (error) {
|
||||
console.error("[v0] Error in ReadPage:", error)
|
||||
console.error("[Karuo] Error in ReadPage:", error)
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user