"use client" import { useState, useRef, useEffect } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { bookData } from "@/lib/book-data" import { FileText, BookOpen, Settings2, ChevronRight, CheckCircle, Edit3, Save, X, RefreshCw, Link2, Download, Upload, Eye, Database, Plus, Image as ImageIcon, Trash2, Search, } from "lucide-react" interface EditingSection { id: string title: string price: number content?: string filePath?: string isNew?: boolean partId?: string chapterId?: string isFree?: boolean // 是否免费章节 } export default function ContentPage() { const [expandedParts, setExpandedParts] = useState(["part-1"]) const [editingSection, setEditingSection] = useState(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 [showNewSectionModal, setShowNewSectionModal] = useState(false) const [importData, setImportData] = useState("") const [isLoadingContent, setIsLoadingContent] = useState(false) const [isSaving, setIsSaving] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [searchResults, setSearchResults] = useState([]) const [isSearching, setIsSearching] = useState(false) const [uploadingImage, setUploadingImage] = useState(false) const fileInputRef = useRef(null) const imageInputRef = useRef(null) // 新建章节表单 const [newSection, setNewSection] = useState({ id: "", title: "", price: 1, partId: "part-1", chapterId: "chapter-1", content: "", }) const togglePart = (partId: string) => { setExpandedParts((prev) => (prev.includes(partId) ? prev.filter((id) => id !== partId) : [...prev, partId])) } const totalSections = bookData.reduce( (sum, part) => sum + part.chapters.reduce((cSum, ch) => cSum + ch.sections.length, 0), 0, ) // 读取章节内容 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: data.section.title || section.title, price: data.section.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 = async () => { if (!editingSection) return setIsSaving(true) try { // 自动去掉内容中的重复标题(如# 1.2 xxx 或 # 1.4 人性的三角结构...) let content = editingSection.content || '' // 匹配多种格式的Markdown标题并去掉: // 1. # 1.2 标题内容 // 2. # 1.2 标题内容(多个空格) // 3. ## 1.2 标题内容 const titlePatterns = [ new RegExp(`^#+\\s*${editingSection.id.replace('.', '\\.')}\\s+.*$`, 'gm'), // # 1.4 xxx new RegExp(`^#+\\s*${editingSection.id.replace('.', '\\.')}[::].*$`, 'gm'), // # 1.4:xxx new RegExp(`^#\\s+.*${editingSection.title?.slice(0, 10).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*$`, 'gm') // # xxx标题内容 ] for (const pattern of titlePatterns) { content = content.replace(pattern, '') } content = content.replace(/^\s*\n+/, '').trim() // 去掉开头的空行 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.isFree ? 0 : editingSection.price, content: content, isFree: editingSection.isFree || editingSection.price === 0, 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("保存失败") } finally { setIsSaving(false) } } // 创建新章节 const handleCreateSection = async () => { if (!newSection.id || !newSection.title) { alert("请填写章节ID和标题") return } setIsSaving(true) try { const res = await fetch('/api/db/book', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: newSection.id, title: newSection.title, price: newSection.price, content: newSection.content, partId: newSection.partId, chapterId: newSection.chapterId, saveToFile: false, // 新建章节暂不保存到文件系统 }) }) const data = await res.json() if (data.success) { alert(`章节创建成功: ${newSection.title}`) setShowNewSectionModal(false) setNewSection({ id: "", title: "", price: 1, partId: "part-1", chapterId: "chapter-1", content: "" }) } else { alert("创建失败: " + (data.error || "未知错误")) } } catch (error) { console.error("Create section error:", error) alert("创建失败") } finally { setIsSaving(false) } } // 上传图片 const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return setUploadingImage(true) try { const formData = new FormData() formData.append('file', file) formData.append('folder', 'book-images') const res = await fetch('/api/upload', { method: 'POST', body: formData }) const data = await res.json() if (data.success) { // 插入图片Markdown到内容 const imageMarkdown = `![${file.name}](${data.data.url})` if (editingSection) { setEditingSection({ ...editingSection, content: (editingSection.content || '') + '\n\n' + imageMarkdown }) } alert(`图片上传成功: ${data.data.url}`) } else { alert("上传失败: " + (data.error || "未知错误")) } } catch (error) { console.error("Image upload error:", error) alert("上传失败") } finally { setUploadingImage(false) if (imageInputRef.current) { imageInputRef.current.value = '' } } } // 搜索内容 const handleSearch = async () => { if (!searchQuery.trim()) return setIsSearching(true) try { const res = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`) const data = await res.json() if (data.success) { setSearchResults(data.data.results || []) } else { alert("搜索失败: " + (data.error || "未知错误")) } } catch (error) { console.error("Search error:", error) alert("搜索失败") } finally { setIsSearching(false) } } // 同步到数据库 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) => { 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', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ adminToken: 'init_db_2025' }) }) const data = await res.json() if (data.success) { alert(data.data?.message || '初始化成功') } else { alert("初始化失败: " + (data.error || "未知错误")) } } catch (error) { console.error("Init database error:", error) alert("初始化失败") } finally { setIsInitializing(false) } } const handleSyncFeishu = async () => { if (!feishuDocUrl) { alert("请输入飞书文档链接") return } setIsSyncing(true) await new Promise((resolve) => setTimeout(resolve, 2000)) setIsSyncing(false) setShowFeishuModal(false) alert("飞书文档同步成功!") } return (

内容管理

共 {bookData.length} 篇 · {totalSections} 节内容

{/* 导入弹窗 */} 导入章节数据

• JSON格式: 直接导入章节数据
• TXT/MD格式: 自动解析为章节内容