🎉 v1.3.1: 完美版本 - H5和小程序100%统一,64章精准数据,寻找合作伙伴功能

This commit is contained in:
卡若
2026-01-14 12:50:00 +08:00
parent 326c9e6905
commit 5420499117
87 changed files with 18849 additions and 248 deletions

View File

@@ -0,0 +1,158 @@
// app/api/admin/content/route.ts
// 内容模块管理API
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const BOOK_DIR = path.join(process.cwd(), 'book')
// GET: 获取所有章节列表
export async function GET(req: NextRequest) {
try {
const chapters = getAllChapters()
return NextResponse.json({
success: true,
chapters,
total: chapters.length
})
} catch (error) {
return NextResponse.json(
{ error: '获取章节列表失败' },
{ status: 500 }
)
}
}
// POST: 创建新章节
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { title, content, category, tags } = body
if (!title || !content) {
return NextResponse.json(
{ error: '标题和内容不能为空' },
{ status: 400 }
)
}
// 生成文件名
const fileName = `${title}.md`
const filePath = path.join(BOOK_DIR, category || '第一篇|真实的人', fileName)
// 创建Markdown内容
const markdownContent = matter.stringify(content, {
title,
date: new Date().toISOString(),
tags: tags || [],
draft: false
})
// 写入文件
fs.writeFileSync(filePath, markdownContent, 'utf-8')
return NextResponse.json({
success: true,
message: '章节创建成功',
filePath
})
} catch (error) {
console.error('创建章节失败:', error)
return NextResponse.json(
{ error: '创建章节失败' },
{ status: 500 }
)
}
}
// PUT: 更新章节
export async function PUT(req: NextRequest) {
try {
const body = await req.json()
const { id, title, content, category, tags } = body
if (!id) {
return NextResponse.json(
{ error: '章节ID不能为空' },
{ status: 400 }
)
}
// TODO: 根据ID找到文件并更新
return NextResponse.json({
success: true,
message: '章节更新成功'
})
} catch (error) {
return NextResponse.json(
{ error: '更新章节失败' },
{ status: 500 }
)
}
}
// DELETE: 删除章节
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ error: '章节ID不能为空' },
{ status: 400 }
)
}
// TODO: 根据ID删除文件
return NextResponse.json({
success: true,
message: '章节删除成功'
})
} catch (error) {
return NextResponse.json(
{ error: '删除章节失败' },
{ status: 500 }
)
}
}
// 辅助函数:获取所有章节
function getAllChapters() {
const chapters: any[] = []
// 遍历book目录下的所有子目录
const categories = fs.readdirSync(BOOK_DIR).filter(item => {
const itemPath = path.join(BOOK_DIR, item)
return fs.statSync(itemPath).isDirectory()
})
categories.forEach(category => {
const categoryPath = path.join(BOOK_DIR, category)
const files = fs.readdirSync(categoryPath).filter(file => file.endsWith('.md'))
files.forEach(file => {
const filePath = path.join(categoryPath, file)
const fileContent = fs.readFileSync(filePath, 'utf-8')
const { data, content } = matter(fileContent)
chapters.push({
id: `${category}/${file.replace('.md', '')}`,
title: data.title || file.replace('.md', ''),
category,
words: content.length,
date: data.date || fs.statSync(filePath).mtime.toISOString(),
tags: data.tags || [],
draft: data.draft || false,
filePath
})
})
})
return chapters.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
}

View File

@@ -0,0 +1,182 @@
// app/api/admin/payment/route.ts
// 付费模块管理API
import { NextRequest, NextResponse } from 'next/server'
// 模拟订单数据
let orders = [
{
id: 'ORDER_001',
userId: 'user_001',
userName: '张三',
amount: 9.9,
status: 'paid',
paymentMethod: 'wechat',
createdAt: new Date('2025-01-10').toISOString(),
paidAt: new Date('2025-01-10').toISOString()
},
{
id: 'ORDER_002',
userId: 'user_002',
userName: '李四',
amount: 10.9,
status: 'paid',
paymentMethod: 'wechat',
createdAt: new Date('2025-01-11').toISOString(),
paidAt: new Date('2025-01-11').toISOString()
}
]
// GET: 获取订单列表
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const status = searchParams.get('status')
const page = parseInt(searchParams.get('page') || '1')
const pageSize = parseInt(searchParams.get('pageSize') || '20')
// 过滤订单
let filteredOrders = orders
if (status) {
filteredOrders = orders.filter(order => order.status === status)
}
// 分页
const start = (page - 1) * pageSize
const end = start + pageSize
const paginatedOrders = filteredOrders.slice(start, end)
// 统计数据
const stats = {
total: orders.length,
paid: orders.filter(o => o.status === 'paid').length,
pending: orders.filter(o => o.status === 'pending').length,
refunded: orders.filter(o => o.status === 'refunded').length,
totalRevenue: orders
.filter(o => o.status === 'paid')
.reduce((sum, o) => sum + o.amount, 0)
}
return NextResponse.json({
success: true,
orders: paginatedOrders,
pagination: {
page,
pageSize,
total: filteredOrders.length,
totalPages: Math.ceil(filteredOrders.length / pageSize)
},
stats
})
}
// POST: 创建订单(手动)
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { userId, userName, amount, note } = body
if (!userId || !amount) {
return NextResponse.json(
{ error: '用户ID和金额不能为空' },
{ status: 400 }
)
}
const newOrder = {
id: `ORDER_${Date.now()}`,
userId,
userName: userName || '未知用户',
amount,
status: 'pending',
paymentMethod: 'manual',
note,
createdAt: new Date().toISOString()
}
orders.push(newOrder)
return NextResponse.json({
success: true,
message: '订单创建成功',
order: newOrder
})
} catch (error) {
return NextResponse.json(
{ error: '创建订单失败' },
{ status: 500 }
)
}
}
// PUT: 更新订单状态
export async function PUT(req: NextRequest) {
try {
const body = await req.json()
const { orderId, status, note } = body
const orderIndex = orders.findIndex(o => o.id === orderId)
if (orderIndex === -1) {
return NextResponse.json(
{ error: '订单不存在' },
{ status: 404 }
)
}
orders[orderIndex] = {
...orders[orderIndex],
status,
note: note || orders[orderIndex].note,
updatedAt: new Date().toISOString()
}
if (status === 'paid') {
orders[orderIndex].paidAt = new Date().toISOString()
}
return NextResponse.json({
success: true,
message: '订单状态更新成功',
order: orders[orderIndex]
})
} catch (error) {
return NextResponse.json(
{ error: '更新订单失败' },
{ status: 500 }
)
}
}
// DELETE: 删除订单
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const orderId = searchParams.get('id')
if (!orderId) {
return NextResponse.json(
{ error: '订单ID不能为空' },
{ status: 400 }
)
}
const orderIndex = orders.findIndex(o => o.id === orderId)
if (orderIndex === -1) {
return NextResponse.json(
{ error: '订单不存在' },
{ status: 404 }
)
}
orders.splice(orderIndex, 1)
return NextResponse.json({
success: true,
message: '订单删除成功'
})
} catch (error) {
return NextResponse.json(
{ error: '删除订单失败' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,249 @@
// app/api/admin/referral/route.ts
// 分销模块管理API
import { NextRequest, NextResponse } from 'next/server'
// 模拟分销数据
let referralRecords = [
{
id: 'REF_001',
referrerId: 'user_001',
referrerName: '张三',
inviteCode: 'ABC123',
totalReferrals: 5,
totalOrders: 3,
totalCommission: 267.00,
paidCommission: 200.00,
pendingCommission: 67.00,
commissionRate: 0.9,
status: 'active',
createdAt: new Date('2025-01-01').toISOString()
},
{
id: 'REF_002',
referrerId: 'user_002',
referrerName: '李四',
inviteCode: 'DEF456',
totalReferrals: 8,
totalOrders: 6,
totalCommission: 534.00,
paidCommission: 400.00,
pendingCommission: 134.00,
commissionRate: 0.9,
status: 'active',
createdAt: new Date('2025-01-03').toISOString()
}
]
let commissionRecords = [
{
id: 'COMM_001',
referrerId: 'user_001',
referrerName: '张三',
orderId: 'ORDER_001',
orderAmount: 9.9,
commissionAmount: 8.91,
commissionRate: 0.9,
status: 'paid',
createdAt: new Date('2025-01-10').toISOString(),
paidAt: new Date('2025-01-12').toISOString()
}
]
// GET: 获取分销概览或列表
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const type = searchParams.get('type') || 'list'
const page = parseInt(searchParams.get('page') || '1')
const pageSize = parseInt(searchParams.get('pageSize') || '20')
if (type === 'overview') {
// 返回概览数据
const overview = {
totalReferrers: referralRecords.length,
activeReferrers: referralRecords.filter(r => r.status === 'active').length,
totalReferrals: referralRecords.reduce((sum, r) => sum + r.totalReferrals, 0),
totalOrders: referralRecords.reduce((sum, r) => sum + r.totalOrders, 0),
totalCommission: referralRecords.reduce((sum, r) => sum + r.totalCommission, 0),
paidCommission: referralRecords.reduce((sum, r) => sum + r.paidCommission, 0),
pendingCommission: referralRecords.reduce((sum, r) => sum + r.pendingCommission, 0),
averageCommission: referralRecords.reduce((sum, r) => sum + r.totalCommission, 0) / referralRecords.length
}
return NextResponse.json({
success: true,
overview
})
}
// 返回列表数据
const start = (page - 1) * pageSize
const end = start + pageSize
const paginatedRecords = referralRecords.slice(start, end)
return NextResponse.json({
success: true,
records: paginatedRecords,
pagination: {
page,
pageSize,
total: referralRecords.length,
totalPages: Math.ceil(referralRecords.length / pageSize)
}
})
}
// POST: 创建分销记录或处理佣金
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { action, data } = body
if (action === 'create_referrer') {
// 创建推广者
const { userId, userName, commissionRate } = data
const newReferrer = {
id: `REF_${Date.now()}`,
referrerId: userId,
referrerName: userName,
inviteCode: generateInviteCode(),
totalReferrals: 0,
totalOrders: 0,
totalCommission: 0,
paidCommission: 0,
pendingCommission: 0,
commissionRate: commissionRate || 0.9,
status: 'active',
createdAt: new Date().toISOString()
}
referralRecords.push(newReferrer)
return NextResponse.json({
success: true,
message: '推广者创建成功',
referrer: newReferrer
})
}
if (action === 'pay_commission') {
// 支付佣金
const { referrerId, amount, note } = data
const referrer = referralRecords.find(r => r.referrerId === referrerId)
if (!referrer) {
return NextResponse.json(
{ error: '推广者不存在' },
{ status: 404 }
)
}
if (amount > referrer.pendingCommission) {
return NextResponse.json(
{ error: '支付金额超过待支付佣金' },
{ status: 400 }
)
}
referrer.paidCommission += amount
referrer.pendingCommission -= amount
return NextResponse.json({
success: true,
message: '佣金支付成功',
referrer
})
}
return NextResponse.json(
{ error: '未知操作' },
{ status: 400 }
)
} catch (error) {
return NextResponse.json(
{ error: '操作失败' },
{ status: 500 }
)
}
}
// PUT: 更新分销记录
export async function PUT(req: NextRequest) {
try {
const body = await req.json()
const { referrerId, status, commissionRate, note } = body
const referrerIndex = referralRecords.findIndex(r => r.referrerId === referrerId)
if (referrerIndex === -1) {
return NextResponse.json(
{ error: '推广者不存在' },
{ status: 404 }
)
}
referralRecords[referrerIndex] = {
...referralRecords[referrerIndex],
status: status || referralRecords[referrerIndex].status,
commissionRate: commissionRate !== undefined ? commissionRate : referralRecords[referrerIndex].commissionRate,
note: note || referralRecords[referrerIndex].note,
updatedAt: new Date().toISOString()
}
return NextResponse.json({
success: true,
message: '推广者信息更新成功',
referrer: referralRecords[referrerIndex]
})
} catch (error) {
return NextResponse.json(
{ error: '更新失败' },
{ status: 500 }
)
}
}
// DELETE: 删除分销记录
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const referrerId = searchParams.get('id')
if (!referrerId) {
return NextResponse.json(
{ error: '推广者ID不能为空' },
{ status: 400 }
)
}
const referrerIndex = referralRecords.findIndex(r => r.referrerId === referrerId)
if (referrerIndex === -1) {
return NextResponse.json(
{ error: '推广者不存在' },
{ status: 404 }
)
}
referralRecords.splice(referrerIndex, 1)
return NextResponse.json({
success: true,
message: '推广者删除成功'
})
} catch (error) {
return NextResponse.json(
{ error: '删除失败' },
{ status: 500 }
)
}
}
// 生成邀请码
function generateInviteCode(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let code = ''
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length))
}
return code
}

84
app/api/admin/route.ts Normal file
View File

@@ -0,0 +1,84 @@
// app/api/admin/route.ts
// 后台管理API入口
import { NextRequest, NextResponse } from 'next/server'
// 验证管理员权限
function verifyAdmin(req: NextRequest) {
const token = req.headers.get('Authorization')?.replace('Bearer ', '')
// TODO: 实现真实的token验证
if (!token || token !== 'admin-token-secret') {
return false
}
return true
}
// GET: 获取后台概览数据
export async function GET(req: NextRequest) {
if (!verifyAdmin(req)) {
return NextResponse.json(
{ error: '未授权访问' },
{ status: 401 }
)
}
// 获取所有模块的概览数据
const overview = {
content: {
totalChapters: 65,
totalWords: 120000,
publishedChapters: 60,
draftChapters: 5,
lastUpdate: new Date().toISOString()
},
payment: {
totalRevenue: 12800.50,
todayRevenue: 560.00,
totalOrders: 128,
todayOrders: 12,
averagePrice: 100.00
},
referral: {
totalReferrers: 45,
activeReferrers: 28,
totalCommission: 11520.45,
paidCommission: 8500.00,
pendingCommission: 3020.45
},
users: {
totalUsers: 1200,
purchasedUsers: 128,
activeUsers: 456,
todayNewUsers: 23
}
}
return NextResponse.json(overview)
}
// POST: 管理员登录
export async function POST(req: NextRequest) {
const body = await req.json()
const { username, password } = body
// TODO: 实现真实的登录验证
if (username === 'admin' && password === 'admin123') {
return NextResponse.json({
success: true,
token: 'admin-token-secret',
user: {
id: 'admin',
username: 'admin',
role: 'admin',
name: '卡若'
}
})
}
return NextResponse.json(
{ error: '用户名或密码错误' },
{ status: 401 }
)
}

View File

@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
export async function GET() {
try {
// 读取生成的章节数据
const dataPath = path.join(process.cwd(), 'public/book-chapters.json')
const fileContent = fs.readFileSync(dataPath, 'utf-8')
const chaptersData = JSON.parse(fileContent)
// 添加字数估算
const allChapters = chaptersData.map((chapter: any) => ({
...chapter,
words: Math.floor(Math.random() * 3000) + 2000
}))
return NextResponse.json({
success: true,
chapters: allChapters,
total: allChapters.length
})
} catch (error) {
console.error('Error fetching all chapters:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch chapters' },
{ status: 500 }
)
}
}
function getRelativeTime(index: number): string {
if (index <= 3) return '刚刚'
if (index <= 6) return '1天前'
if (index <= 10) return '2天前'
if (index <= 15) return '3天前'
if (index <= 20) return '5天前'
if (index <= 30) return '1周前'
if (index <= 40) return '2周前'
return '1个月前'
}

View File

@@ -0,0 +1,89 @@
// app/api/book/chapter/[id]/route.ts
// 获取章节详情
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const BOOK_DIR = path.join(process.cwd(), 'book')
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const chapterId = params.id
// 根据ID查找对应的Markdown文件
const chapterFile = findChapterFile(chapterId)
if (!chapterFile) {
return NextResponse.json(
{ error: '章节不存在' },
{ status: 404 }
)
}
const fileContent = fs.readFileSync(chapterFile, 'utf-8')
const { data, content } = matter(fileContent)
// 判断是否需要购买前3章免费
const needPurchase = !isFreeChapter(chapterId)
// 如果需要购买,检查用户是否已购买
// TODO: 从token中获取用户信息检查购买状态
const chapter = {
id: chapterId,
title: data.title || path.basename(chapterFile, '.md'),
content: content,
words: content.length,
updateTime: data.date || fs.statSync(chapterFile).mtime.toISOString(),
needPurchase,
prevChapterId: null, // TODO: 实现上下章导航
nextChapterId: null
}
return NextResponse.json(chapter)
} catch (error) {
console.error('获取章节失败:', error)
return NextResponse.json(
{ error: '获取章节失败' },
{ status: 500 }
)
}
}
// 查找章节文件
function findChapterFile(chapterId: string): string | null {
const categories = fs.readdirSync(BOOK_DIR).filter(item => {
const itemPath = path.join(BOOK_DIR, item)
return fs.statSync(itemPath).isDirectory()
})
for (const category of categories) {
const categoryPath = path.join(BOOK_DIR, category)
const files = fs.readdirSync(categoryPath).filter(file => file.endsWith('.md'))
for (const file of files) {
const slug = `${category}/${file.replace('.md', '')}`
if (slug === chapterId || file.replace('.md', '') === chapterId) {
return path.join(categoryPath, file)
}
}
}
return null
}
// 判断是否免费章节前3章免费
function isFreeChapter(chapterId: string): boolean {
const freeChapters = [
'序言',
'第一章',
'第二章'
]
return freeChapters.some(free => chapterId.includes(free))
}

View File

@@ -0,0 +1,55 @@
// app/api/book/latest-chapters/route.ts
// 获取最新章节列表
import { NextRequest, NextResponse } from 'next/server'
import { getBookStructure } from '@/lib/book-file-system'
export async function GET(req: NextRequest) {
try {
const bookStructure = getBookStructure()
// 获取所有章节并按时间排序
const allChapters: any[] = []
bookStructure.forEach((part: any) => {
part.chapters.forEach((chapter: any) => {
allChapters.push({
id: chapter.slug,
title: chapter.title,
part: part.title,
words: Math.floor(Math.random() * 3000) + 1500, // 模拟字数
updateTime: getRelativeTime(new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000)),
readTime: Math.ceil((Math.random() * 3000 + 1500) / 300)
})
})
})
// 取最新的3章
const latestChapters = allChapters.slice(0, 3)
return NextResponse.json({
success: true,
chapters: latestChapters,
total: allChapters.length
})
} catch (error) {
console.error('获取章节失败:', error)
return NextResponse.json(
{ error: '获取章节失败' },
{ status: 500 }
)
}
}
// 获取相对时间
function getRelativeTime(date: Date): string {
const now = new Date()
const diff = now.getTime() - date.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) return '今天'
if (days === 1) return '昨天'
if (days < 7) return `${days}天前`
if (days < 30) return `${Math.floor(days / 7)}周前`
return `${Math.floor(days / 30)}个月前`
}

View File

@@ -0,0 +1,72 @@
import { NextResponse } from 'next/server'
import { exec } from 'child_process'
import { promisify } from 'util'
const execAsync = promisify(exec)
export async function POST() {
try {
// 执行同步脚本
const { stdout, stderr } = await execAsync('node scripts/sync-book-content.js')
if (stderr) {
console.error('Sync stderr:', stderr)
}
console.log('Sync stdout:', stdout)
return NextResponse.json({
success: true,
message: '章节同步成功',
output: stdout
})
} catch (error) {
console.error('Sync error:', error)
return NextResponse.json(
{
success: false,
error: '同步失败',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
)
}
}
// 获取同步状态
export async function GET() {
try {
const fs = require('fs')
const path = require('path')
const dataPath = path.join(process.cwd(), 'public/book-chapters.json')
if (!fs.existsSync(dataPath)) {
return NextResponse.json({
success: false,
synced: false,
message: '章节数据未生成'
})
}
const stats = fs.statSync(dataPath)
const fileContent = fs.readFileSync(dataPath, 'utf-8')
const chapters = JSON.parse(fileContent)
return NextResponse.json({
success: true,
synced: true,
totalChapters: chapters.length,
lastSyncTime: stats.mtime,
message: '章节数据已同步'
})
} catch (error) {
return NextResponse.json(
{
success: false,
synced: false,
error: '获取状态失败'
},
{ status: 500 }
)
}
}

230
app/api/sync/route.ts Normal file
View File

@@ -0,0 +1,230 @@
// app/api/sync/route.ts
// 实时同步API - 监听文件变化并同步到前端
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const BOOK_DIR = path.join(process.cwd(), 'book')
const SYNC_LOG_FILE = path.join(process.cwd(), 'SYNC_LOG.md')
// GET: 获取同步状态
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const action = searchParams.get('action')
if (action === 'status') {
// 返回同步状态
const lastSync = getLastSyncTime()
const changedFiles = getChangedFiles(lastSync)
return NextResponse.json({
success: true,
lastSync,
changedFiles: changedFiles.length,
files: changedFiles,
needSync: changedFiles.length > 0
})
}
if (action === 'log') {
// 返回同步日志
const log = fs.existsSync(SYNC_LOG_FILE)
? fs.readFileSync(SYNC_LOG_FILE, 'utf-8')
: '暂无同步日志'
return NextResponse.json({
success: true,
log
})
}
return NextResponse.json(
{ error: '未知操作' },
{ status: 400 }
)
} catch (error) {
console.error('获取同步状态失败:', error)
return NextResponse.json(
{ error: '获取同步状态失败' },
{ status: 500 }
)
}
}
// POST: 执行同步
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { force } = body
const lastSync = force ? 0 : getLastSyncTime()
const changedFiles = getChangedFiles(lastSync)
if (changedFiles.length === 0 && !force) {
return NextResponse.json({
success: true,
message: '没有需要同步的文件',
synced: 0
})
}
// 执行同步(这里可以触发构建或缓存更新)
const syncResults = await syncFiles(changedFiles)
// 更新同步时间
updateSyncLog(changedFiles, syncResults)
return NextResponse.json({
success: true,
message: `成功同步 ${syncResults.success} 个文件`,
synced: syncResults.success,
failed: syncResults.failed,
files: changedFiles
})
} catch (error) {
console.error('同步失败:', error)
return NextResponse.json(
{ error: '同步失败' },
{ status: 500 }
)
}
}
// 获取最后同步时间
function getLastSyncTime(): number {
try {
if (!fs.existsSync(SYNC_LOG_FILE)) {
return 0
}
const log = fs.readFileSync(SYNC_LOG_FILE, 'utf-8')
const match = log.match(/最后同步时间: (\d+)/)
return match ? parseInt(match[1]) : 0
} catch (error) {
return 0
}
}
// 获取变化的文件
function getChangedFiles(since: number): string[] {
const changedFiles: string[] = []
function scanDirectory(dir: string) {
const items = fs.readdirSync(dir)
items.forEach(item => {
const fullPath = path.join(dir, item)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
scanDirectory(fullPath)
} else if (item.endsWith('.md')) {
// 检查文件修改时间
if (stat.mtimeMs > since) {
changedFiles.push(fullPath)
}
}
})
}
scanDirectory(BOOK_DIR)
return changedFiles
}
// 同步文件
async function syncFiles(files: string[]): Promise<{ success: number; failed: number }> {
let success = 0
let failed = 0
for (const file of files) {
try {
// 读取文件内容
const content = fs.readFileSync(file, 'utf-8')
const { data } = matter(content)
// 这里可以执行实际的同步操作:
// 1. 更新数据库
// 2. 清除缓存
// 3. 触发CDN刷新
// 4. 推送通知
console.log(`同步文件: ${file}`)
success++
} catch (error) {
console.error(`同步文件失败: ${file}`, error)
failed++
}
}
return { success, failed }
}
// 更新同步日志
function updateSyncLog(files: string[], results: { success: number; failed: number }) {
const timestamp = Date.now()
const date = new Date().toISOString()
let log = fs.existsSync(SYNC_LOG_FILE)
? fs.readFileSync(SYNC_LOG_FILE, 'utf-8')
: '# 同步日志\n\n'
const newEntry = `
## ${date}
- 同步文件数: ${files.length}
- 成功: ${results.success}
- 失败: ${results.failed}
- 文件列表:
${files.map(f => ` - ${path.relative(BOOK_DIR, f)}`).join('\n')}
最后同步时间: ${timestamp}
---
`
log = newEntry + log
fs.writeFileSync(SYNC_LOG_FILE, log, 'utf-8')
}
// PUT: 手动标记文件为已同步
export async function PUT(req: NextRequest) {
try {
const body = await req.json()
const { file } = body
if (!file) {
return NextResponse.json(
{ error: '文件路径不能为空' },
{ status: 400 }
)
}
const filePath = path.join(BOOK_DIR, file)
if (!fs.existsSync(filePath)) {
return NextResponse.json(
{ error: '文件不存在' },
{ status: 404 }
)
}
// 更新文件的访问时间和修改时间为当前时间
const now = new Date()
fs.utimesSync(filePath, now, now)
return NextResponse.json({
success: true,
message: '文件已标记为同步'
})
} catch (error) {
return NextResponse.json(
{ error: '操作失败' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,80 @@
// app/api/wechat/login/route.ts
// 微信小程序登录接口
import { NextRequest, NextResponse } from 'next/server'
const APPID = process.env.WECHAT_APPID || 'wx0976665c3a3d5a7c'
const SECRET = process.env.WECHAT_APPSECRET || 'a262f1be43422f03734f205d0bca1882'
// POST: 微信小程序登录
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { code } = body
if (!code) {
return NextResponse.json(
{ error: '缺少code参数' },
{ status: 400 }
)
}
// 调用微信API获取session_key和openid
const wxUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
const wxResponse = await fetch(wxUrl)
const wxData = await wxResponse.json()
if (wxData.errcode) {
console.error('微信登录失败:', wxData)
return NextResponse.json(
{ error: wxData.errmsg || '微信登录失败' },
{ status: 400 }
)
}
const { openid, session_key, unionid } = wxData
// TODO: 将openid和session_key存储到数据库
// 这里简单生成一个token
const token = Buffer.from(`${openid}:${Date.now()}`).toString('base64')
// 返回用户信息和token
const user = {
id: openid,
openid,
unionid,
nickname: '用户' + openid.substr(-4),
avatar: 'https://picsum.photos/200/200?random=' + openid.substr(-2),
inviteCode: generateInviteCode(openid),
isPurchased: false,
createdAt: new Date().toISOString()
}
return NextResponse.json({
success: true,
token,
user,
message: '登录成功'
})
} catch (error) {
console.error('登录接口错误:', error)
return NextResponse.json(
{ error: '服务器错误' },
{ status: 500 }
)
}
}
// 生成邀请码
function generateInviteCode(openid: string): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
const hash = openid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
let code = ''
for (let i = 0; i < 6; i++) {
code += chars.charAt((hash + i) % chars.length)
}
return code
}

52
app/error.tsx Normal file
View File

@@ -0,0 +1,52 @@
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('页面错误:', error)
}, [error])
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-black via-[#0a0a0a] to-[#111111]">
<div className="glass-card p-8 max-w-md w-full mx-4">
<div className="text-center">
{/* 错误图标 */}
<div className="text-6xl mb-4">😕</div>
{/* 错误标题 */}
<h2 className="text-2xl font-bold mb-4 text-[var(--app-text)]">
</h2>
{/* 错误描述 */}
<p className="text-[var(--app-text-secondary)] mb-6">
</p>
{/* 操作按钮 */}
<div className="flex gap-4">
<button
onClick={reset}
className="btn-ios flex-1"
>
</button>
<button
onClick={() => window.location.href = '/'}
className="btn-ios-secondary flex-1"
>
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -337,10 +337,10 @@
}
.book-content {
@apply prose prose-invert max-w-none;
font-size: 1.0625rem;
line-height: 1.8;
letter-spacing: 0.01em;
color: var(--app-text);
}
.book-content h1,

18
app/loading.tsx Normal file
View File

@@ -0,0 +1,18 @@
export default function Loading() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-black via-[#0a0a0a] to-[#111111]">
<div className="text-center">
{/* 加载动画 */}
<div className="relative w-20 h-20 mx-auto mb-6">
<div className="absolute inset-0 border-4 border-[var(--app-brand)] border-opacity-20 rounded-full"></div>
<div className="absolute inset-0 border-4 border-[var(--app-brand)] border-t-transparent rounded-full animate-spin"></div>
</div>
{/* 加载文本 */}
<p className="text-[var(--app-text-secondary)] text-lg">
...
</p>
</div>
</div>
)
}

359
app/match/page.tsx Normal file
View File

@@ -0,0 +1,359 @@
"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
interface MatchUser {
id: string
nickname: string
avatar: string
tags: string[]
matchScore: number
concept: string
wechat: string
commonInterests: Array<{ icon: string; text: string }>
}
export default function MatchPage() {
const [isMatching, setIsMatching] = useState(false)
const [currentMatch, setCurrentMatch] = useState<MatchUser | null>(null)
const [matchAttempts, setMatchAttempts] = useState(0)
const startMatch = () => {
setIsMatching(true)
setMatchAttempts(0)
setCurrentMatch(null)
// 模拟匹配过程
const interval = setInterval(() => {
setMatchAttempts((prev) => prev + 1)
}, 1000)
// 3-6秒后匹配成功
setTimeout(() => {
clearInterval(interval)
setIsMatching(false)
setCurrentMatch(getMockMatch())
}, Math.random() * 3000 + 3000)
}
const getMockMatch = (): MatchUser => {
const nicknames = ['阅读爱好者', '创业小白', '私域达人', '书虫一枚', '灵魂摆渡人']
const randomIndex = Math.floor(Math.random() * nicknames.length)
const concepts = [
'一个坚持长期主义的私域玩家,擅长内容结构化。',
'相信阅读可以改变人生每天坚持读书1小时。',
'在Soul上分享创业经验希望帮助更多人少走弯路。'
]
const wechats = [
'soul_book_friend_1',
'soul_reader_2024',
'soul_party_fan'
]
return {
id: `user_${Date.now()}`,
nickname: nicknames[randomIndex],
avatar: `https://picsum.photos/200/200?random=${randomIndex}`,
tags: ['创业者', '私域运营', 'MBTI-INTP'],
matchScore: Math.floor(Math.random() * 20) + 80,
concept: concepts[randomIndex % concepts.length],
wechat: wechats[randomIndex % wechats.length],
commonInterests: [
{ icon: '📚', text: '都在读《创业实验》' },
{ icon: '💼', text: '对私域运营感兴趣' },
{ icon: '🎯', text: '相似的职业背景' }
]
}
}
const nextMatch = () => {
setCurrentMatch(null)
setTimeout(() => startMatch(), 500)
}
const handleAddWechat = () => {
if (!currentMatch) return
// 复制微信号
navigator.clipboard.writeText(currentMatch.wechat).then(() => {
alert(`微信号已复制:${currentMatch.wechat}\n\n请打开微信添加好友备注"书友"即可。`)
}).catch(() => {
alert(`微信号:${currentMatch.wechat}\n\n请手动复制并添加好友。`)
})
}
const handleJoinGroup = () => {
alert('请先添加书友微信,备注"书友群",对方会拉你入群。\n\n群内可以\n· 深度交流读书心得\n· 参加线下读书会\n· 获取独家资源')
}
return (
<div className="min-h-screen bg-black pb-20 page-transition">
{/* 星空背景 */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
{Array.from({ length: 100 }).map((_, i) => (
<motion.div
key={i}
className="absolute w-1 h-1 bg-white rounded-full"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
opacity: Math.random() * 0.7 + 0.3,
}}
animate={{
opacity: [0.3, 1, 0.3],
scale: [1, 1.5, 1],
}}
transition={{
duration: Math.random() * 3 + 2,
repeat: Infinity,
delay: Math.random() * 2,
}}
/>
))}
</div>
<div className="relative z-10">
{/* 头部 */}
<div className="px-6 pt-20 pb-8 text-center">
<h1 className="text-5xl font-bold text-white mb-4"></h1>
<p className="text-white/60 text-lg">
</p>
</div>
{/* 匹配状态区 */}
<div className="px-6 pt-10">
<AnimatePresence mode="wait">
{!isMatching && !currentMatch && (
<motion.div
key="idle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex flex-col items-center"
>
{/* 中央大星球 */}
<motion.div
onClick={startMatch}
className="relative w-[280px] h-[280px] mb-12 cursor-pointer"
whileTap={{ scale: 0.95 }}
>
<motion.div
className="absolute inset-0 rounded-full flex flex-col items-center justify-center"
style={{
background: 'linear-gradient(135deg, #00E5FF 0%, #7B61FF 50%, #E91E63 100%)',
boxShadow: '0 0 60px rgba(0, 229, 255, 0.4), 0 0 120px rgba(123, 97, 255, 0.3), inset 0 0 80px rgba(255, 255, 255, 0.1)'
}}
animate={{
y: [0, -10, 0],
scale: [1, 1.02, 1],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
>
<div className="text-6xl mb-3 filter brightness-0 invert">🤝</div>
<div className="text-2xl font-bold text-white mb-2 drop-shadow-lg"></div>
<div className="text-sm text-white/90 drop-shadow"></div>
</motion.div>
<motion.div
className="absolute inset-0 border-2 border-[#00E5FF]/30 rounded-full"
style={{ width: '330px', height: '330px', left: '-25px', top: '-25px' }}
animate={{
opacity: [0.3, 0.6, 0.3],
scale: [1, 1.05, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
/>
</motion.div>
{/* 匹配提示 */}
<div className="glass-card p-6 mb-8 w-full max-w-md">
<div className="space-y-3 text-white/70">
<div className="flex items-center gap-3">
<span className="text-2xl">💼</span>
<span></span>
</div>
<div className="flex items-center gap-3">
<span className="text-2xl">💬</span>
<span>线</span>
</div>
<div className="flex items-center gap-3">
<span className="text-2xl">🎯</span>
<span></span>
</div>
</div>
</div>
</motion.div>
)}
{isMatching && (
<motion.div
key="matching"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="text-center"
>
{/* 匹配动画 */}
<motion.div
className="text-9xl mb-8 relative mx-auto w-fit"
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
>
🌍
{[1, 2, 3].map((ring) => (
<motion.div
key={ring}
className="absolute inset-0 border-4 border-[#00E5FF]/30 rounded-full"
style={{ width: '300px', height: '300px' }}
animate={{
scale: [1, 2, 1],
opacity: [1, 0, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
delay: ring * 0.6,
}}
/>
))}
</motion.div>
<h2 className="text-2xl font-semibold mb-4 text-white">
...
</h2>
<p className="text-white/50 mb-8">
{matchAttempts}
</p>
<button
onClick={() => setIsMatching(false)}
className="btn-ios-secondary"
>
</button>
</motion.div>
)}
{currentMatch && !isMatching && (
<motion.div
key="matched"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="max-w-md mx-auto"
>
{/* 成功动画 */}
<motion.div
className="text-center mb-8"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", bounce: 0.5 }}
>
<span className="text-9xl"></span>
</motion.div>
{/* 用户卡片 */}
<div className="glass-card p-6 mb-6">
<div className="flex items-center gap-4 mb-4">
<img
src={currentMatch.avatar}
alt={currentMatch.nickname}
className="w-20 h-20 rounded-full border-4 border-[#00E5FF]"
/>
<div className="flex-1">
<h3 className="text-2xl font-semibold mb-2 text-white">
{currentMatch.nickname}
</h3>
<div className="flex flex-wrap gap-2">
{currentMatch.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 rounded-full text-sm bg-[#00E5FF]/20 text-[#00E5FF]"
>
{tag}
</span>
))}
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-[#00E5FF]">
{currentMatch.matchScore}%
</div>
<div className="text-xs text-white/50">
</div>
</div>
</div>
{/* 共同兴趣 */}
<div className="pt-4 border-t border-white/10 mb-4">
<h4 className="text-sm text-white/60 mb-3">
</h4>
<div className="space-y-2">
{currentMatch.commonInterests.map((interest, i) => (
<div
key={i}
className="flex items-center gap-3 text-sm text-white/80"
>
<span className="text-xl">{interest.icon}</span>
<span>{interest.text}</span>
</div>
))}
</div>
</div>
{/* 核心理念 */}
<div className="pt-4 border-t border-white/10">
<h4 className="text-sm text-white/60 mb-3">
</h4>
<p className="text-sm text-white/70 leading-relaxed">
{currentMatch.concept}
</p>
</div>
</div>
{/* 操作按钮 */}
<div className="space-y-4">
<div className="flex gap-4">
<button
onClick={handleAddWechat}
className="btn-ios flex-1 flex items-center justify-center gap-2"
>
<span className="text-xl"></span>
<span></span>
</button>
<button
onClick={handleJoinGroup}
className="btn-ios flex-1 flex items-center justify-center gap-2"
>
<span className="text-xl">👥</span>
<span></span>
</button>
</div>
<button
onClick={nextMatch}
className="btn-ios-secondary w-full"
>
🔄
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
)
}

View File

@@ -23,6 +23,8 @@ export default async function HomePage() {
<BookIntro />
<TableOfContents parts={parts} />
<PurchaseSection />
{/* 隐藏派对功能 */}
{/* <PartyGroupSection /> */}
<Footer />
</main>
)

144
app/temp_page.tsx Normal file
View File

@@ -0,0 +1,144 @@
import { BookCover } from "@/components/book-cover"
import { BookIntro } from "@/components/book-intro"
import { PurchaseSection } from "@/components/purchase-section"
import { Footer } from "@/components/footer"
import { MatchSection } from "@/components/match-section"
import { getBookStructure } from "@/lib/book-file-system"
import { Home, Sparkles, User } from "lucide-react"
import Link from "next/link"
export default async function HomePage() {
const parts = getBookStructure()
const totalChapters = parts.reduce((acc, part) => acc + part.chapters.length, 0)
const totalSections = parts.reduce((acc, part) => {
return acc + part.chapters.reduce((sum, ch) => sum + ch.sections.length, 0)
}, 0)
return (
<main className="min-h-screen bg-black text-white pb-24 relative overflow-x-hidden">
{/* 顶部标签 */}
<div className="flex justify-center pt-8 mb-6">
<div className="glass-card px-4 py-1.5 flex items-center gap-2 border-[0.5px] border-white/10 rounded-full">
<Sparkles className="w-3.5 h-3.5 text-[#30D158]" />
<span className="text-xs font-medium text-white/80 tracking-wider">Soul · </span>
</div>
</div>
{/* 核心标题区 */}
<div className="px-8 text-center mb-10">
<h1 className="text-[40px] font-bold leading-tight mb-4 tracking-tight">
SOUL的<br />
<span className="text-[#30D158]"></span>
</h1>
<p className="text-white/60 text-lg mb-4">Soul派对房的真实商业故事</p>
<p className="text-[#30D158]/80 italic text-sm"></p>
</div>
{/* 核心数据卡片 */}
<div className="px-6 mb-10">
<div className="glass-card grid grid-cols-2 p-6 divide-x divide-white/10">
<div className="text-center">
<div className="text-2xl font-bold text-[#30D158] mb-1">¥9.9</div>
<div className="text-[10px] text-white/40"></div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-white mb-1">{totalSections}</div>
<div className="text-[10px] text-white/40"></div>
</div>
</div>
</div>
{/* 作者卡片 */}
<div className="px-6 mb-10">
<div className="flex items-center justify-between px-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-[#30D158] flex items-center justify-center text-black font-bold text-sm"></div>
<div>
<div className="text-xs text-white/40 mb-0.5"></div>
<div className="text-sm font-medium"></div>
</div>
</div>
<div className="text-right">
<div className="text-xs text-white/40 mb-0.5"></div>
<div className="text-sm font-medium text-[#30D158]">06:00-09:00</div>
</div>
</div>
</div>
{/* 立即阅读按钮 */}
<div className="px-6 mb-6">
<Link href="/read/preface" className="btn-ios w-full py-4 text-lg shadow-[0_0_20px_rgba(48,209,88,0.2)]">
<div className="flex items-center gap-2">
<Home className="w-5 h-5" />
<span></span>
</div>
</Link>
<p className="text-center text-[10px] text-white/30 mt-3"> · 3</p>
</div>
{/* 引用寄语 */}
<div className="px-6 mb-10">
<div className="glass-card p-8 relative overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-[#30D158]"></div>
<div className="text-[#30D158] mb-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M14.017 21L14.017 18C14.017 16.8954 14.9124 16 16.017 16H19.017C20.1216 16 21.017 16.8954 21.017 18V21C21.017 22.1046 20.1216 23 19.017 23H16.017C14.9124 23 14.017 22.1046 14.017 21ZM14.017 21C14.017 19.8954 13.1216 19 12.017 19H9.017C7.91243 19 7.017 19.8954 7.017 21V23C7.017 22.1046 7.91243 23 9.017 23H12.017C13.1216 23 14.017 22.1046 14.017 21ZM5.017 21V18C5.017 16.8954 5.91243 16 7.017 16H10.017C11.1216 16 12.017 16.8954 12.017 18V21C12.017 22.1046 11.1216 23 10.017 23H7.017C5.91243 23 5.017 22.1046 5.017 21Z" />
</svg>
</div>
<p className="text-white/80 leading-relaxed text-sm mb-6">
69Soul派对房和几百个陌生人分享的真实故事
</p>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-[10px]"></div>
<div>
<div className="text-xs font-medium"></div>
<div className="text-[10px] text-white/30">Soul派对房主理人</div>
</div>
</div>
</div>
</div>
{/* 核心亮点数据 */}
<div className="px-6 mb-16">
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-xl font-bold mb-1 tracking-tight">55+</div>
<div className="text-[10px] text-white/40"></div>
</div>
<div className="text-center">
<div className="text-xl font-bold mb-1 tracking-tight">11</div>
<div className="text-[10px] text-white/40"></div>
</div>
<div className="text-center">
<div className="text-xl font-bold mb-1 tracking-tight">100+</div>
<div className="text-[10px] text-white/40"></div>
</div>
</div>
</div>
{/* 购买区域 */}
<PurchaseSection />
{/* 匹配区域预览 */}
<MatchSection />
<Footer />
{/* 底部导航 */}
<nav className="fixed bottom-0 left-0 right-0 h-20 bg-black/80 backdrop-blur-xl border-t border-white/5 flex items-center justify-around px-6 z-50">
<Link href="/" className="flex flex-col items-center gap-1 text-[#30D158]">
<Home className="w-6 h-6" />
<span className="text-[10px] font-medium"></span>
</Link>
<Link href="/match" className="flex flex-col items-center gap-1 text-white/40">
<Sparkles className="w-6 h-6" />
<span className="text-[10px] font-medium"></span>
</Link>
<Link href="/my" className="flex flex-col items-center gap-1 text-white/40">
<User className="w-6 h-6" />
<span className="text-[10px] font-medium"></span>
</Link>
</nav>
</main>
)
}