diff --git a/.gitignore b/.gitignore index 2d75c7b..9553151 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,67 @@ +# Dependencies node_modules/ -.next/ -.env.local +/.pnp +.pnp.js +.yarn/install-state.gz + +# Testing +/coverage + +# Next.js +/.next/ +/out/ +next-env.d.ts + +# Production +/build +/dist + +# Misc .DS_Store -.trae/ +*.pem +.idea +.vscode + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env +.env*.local +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# Turbo +.turbo + +# TypeScript +*.tsbuildinfo + +# 小程序敏感信息 +miniprogram/project.private.config.json + +# 临时文件 +*.tmp *.log +*.swp +*~ + +# 系统文件 +Thumbs.db +Desktop.ini + +# IDE +.vscode/ +.idea/ +*.sublime-* + +# 缓存 +.cache/ +.parcel-cache/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6017696 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# Next.js 应用 Dockerfile +FROM node:18-alpine AS base + +# 安装依赖阶段 +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# 复制依赖文件 +COPY package.json package-lock.json* pnpm-lock.yaml* ./ + +# 优先使用pnpm,如果没有则使用npm +RUN if [ -f pnpm-lock.yaml ]; then \ + corepack enable && corepack prepare pnpm@latest --activate && \ + pnpm install --frozen-lockfile; \ + else \ + npm ci --legacy-peer-deps || npm install --legacy-peer-deps; \ + fi + +# 构建阶段 +FROM base AS builder +WORKDIR /app + +# 启用corepack(如果需要pnpm) +RUN corepack enable || true + +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# 设置环境变量 +ENV NEXT_TELEMETRY_DISABLED 1 + +# 构建应用 - 优先使用pnpm +RUN if [ -f pnpm-lock.yaml ]; then \ + corepack prepare pnpm@latest --activate && pnpm build; \ + else \ + npm run build; \ + fi + +# 生产运行阶段 +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# 复制必要文件 +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] diff --git a/app/api/admin/content/route.ts b/app/api/admin/content/route.ts new file mode 100644 index 0000000..84d54cc --- /dev/null +++ b/app/api/admin/content/route.ts @@ -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()) +} diff --git a/app/api/admin/payment/route.ts b/app/api/admin/payment/route.ts new file mode 100644 index 0000000..cde1bea --- /dev/null +++ b/app/api/admin/payment/route.ts @@ -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 } + ) + } +} diff --git a/app/api/admin/referral/route.ts b/app/api/admin/referral/route.ts new file mode 100644 index 0000000..832229a --- /dev/null +++ b/app/api/admin/referral/route.ts @@ -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 +} diff --git a/app/api/admin/route.ts b/app/api/admin/route.ts new file mode 100644 index 0000000..ae960d7 --- /dev/null +++ b/app/api/admin/route.ts @@ -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 } + ) +} diff --git a/app/api/book/all-chapters/route.ts b/app/api/book/all-chapters/route.ts new file mode 100644 index 0000000..eb91532 --- /dev/null +++ b/app/api/book/all-chapters/route.ts @@ -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个月前' +} diff --git a/app/api/book/chapter/[id]/route.ts b/app/api/book/chapter/[id]/route.ts new file mode 100644 index 0000000..b3e1c79 --- /dev/null +++ b/app/api/book/chapter/[id]/route.ts @@ -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)) +} diff --git a/app/api/book/latest-chapters/route.ts b/app/api/book/latest-chapters/route.ts new file mode 100644 index 0000000..369eaa1 --- /dev/null +++ b/app/api/book/latest-chapters/route.ts @@ -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)}个月前` +} diff --git a/app/api/book/sync/route.ts b/app/api/book/sync/route.ts new file mode 100644 index 0000000..20815c8 --- /dev/null +++ b/app/api/book/sync/route.ts @@ -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 } + ) + } +} diff --git a/app/api/sync/route.ts b/app/api/sync/route.ts new file mode 100644 index 0000000..b391ea2 --- /dev/null +++ b/app/api/sync/route.ts @@ -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 } + ) + } +} diff --git a/app/api/wechat/login/route.ts b/app/api/wechat/login/route.ts new file mode 100644 index 0000000..7a4ce68 --- /dev/null +++ b/app/api/wechat/login/route.ts @@ -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 +} diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..8b88b0a --- /dev/null +++ b/app/error.tsx @@ -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 ( +
+
+
+ {/* 错误图标 */} +
😕
+ + {/* 错误标题 */} +

+ 哎呀,出错了 +

+ + {/* 错误描述 */} +

+ 页面遇到了一些问题,请稍后再试 +

+ + {/* 操作按钮 */} +
+ + +
+
+
+
+ ) +} diff --git a/app/globals.css b/app/globals.css index 5e9537d..c6f5a4d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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, diff --git a/app/loading.tsx b/app/loading.tsx new file mode 100644 index 0000000..8d56e85 --- /dev/null +++ b/app/loading.tsx @@ -0,0 +1,18 @@ +export default function Loading() { + return ( +
+
+ {/* 加载动画 */} +
+
+
+
+ + {/* 加载文本 */} +

+ 加载中... +

+
+
+ ) +} diff --git a/app/match/page.tsx b/app/match/page.tsx new file mode 100644 index 0000000..8ff7560 --- /dev/null +++ b/app/match/page.tsx @@ -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(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 ( +
+ {/* 星空背景 */} +
+ {Array.from({ length: 100 }).map((_, i) => ( + + ))} +
+ +
+ {/* 头部 */} +
+

寻找合作伙伴

+

+ 找到和你一起创业的灵魂 +

+
+ + {/* 匹配状态区 */} +
+ + {!isMatching && !currentMatch && ( + + {/* 中央大星球 */} + + +
🤝
+
开始匹配
+
寻找合作伙伴
+
+ + + + + {/* 匹配提示 */} +
+
+
+ 💼 + 共同的创业方向 +
+
+ 💬 + 实时在线交流 +
+
+ 🎯 + 相似的商业洞察 +
+
+
+
+ )} + + {isMatching && ( + + {/* 匹配动画 */} + + 🌍 + {[1, 2, 3].map((ring) => ( + + ))} + + +

+ 正在寻找志同道合的书友... +

+

+ 已匹配 {matchAttempts} 次 +

+ + +
+ )} + + {currentMatch && !isMatching && ( + + {/* 成功动画 */} + + + + + {/* 用户卡片 */} +
+
+ {currentMatch.nickname} +
+

+ {currentMatch.nickname} +

+
+ {currentMatch.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+ {currentMatch.matchScore}% +
+
+ 匹配度 +
+
+
+ + {/* 共同兴趣 */} +
+

+ 共同兴趣 +

+
+ {currentMatch.commonInterests.map((interest, i) => ( +
+ {interest.icon} + {interest.text} +
+ ))} +
+
+ + {/* 核心理念 */} +
+

+ 核心理念 +

+

+ {currentMatch.concept} +

+
+
+ + {/* 操作按钮 */} +
+
+ + +
+ +
+
+ )} +
+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index 2c8aae2..1432dc3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -23,6 +23,8 @@ export default async function HomePage() { + {/* 隐藏派对功能 */} + {/* */}