'use client' export const runtime = 'edge' import { useState, useEffect } from 'react' import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { JsonEditor } from "@/components/ui/json-editor" import { useToast } from "@/components/ui/use-toast" import { Download, RefreshCw, Save, AlertTriangle, Database, FileText, CheckCircle, XCircle, Info, Upload } from "lucide-react" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" export default function DataManagementPage() { const [navigationData, setNavigationData] = useState('') const [isLoading, setIsLoading] = useState(false) const [isSaving, setIsSaving] = useState(false) const [isRestoreDialogOpen, setIsRestoreDialogOpen] = useState(false) const [isJsonValid, setIsJsonValid] = useState(true) const [jsonError, setJsonError] = useState('') const [dataStats, setDataStats] = useState({ categories: 0, items: 0, size: 0 }) const [editorErrors, setEditorErrors] = useState([]) const [defaultFileStatus, setDefaultFileStatus] = useState({ exists: false, valid: false, itemCount: 0, checked: false }) const { toast } = useToast() // 检查默认文件状态 const checkDefaultFile = async () => { try { const response = await fetch('/api/navigation/check-default') if (response.ok) { const status = await response.json() setDefaultFileStatus({ ...status, checked: true }) } else { setDefaultFileStatus({ exists: false, valid: false, itemCount: 0, checked: true }) } } catch (error) { console.error('Failed to check default file:', error) setDefaultFileStatus({ exists: false, valid: false, itemCount: 0, checked: true }) } } // 验证JSON格式并更新统计信息 const validateJson = (jsonString: string) => { try { const parsed = JSON.parse(jsonString) setIsJsonValid(true) setJsonError('') // 计算统计信息 if (parsed.navigationItems) { const categories = parsed.navigationItems.length let items = 0 parsed.navigationItems.forEach((category: any) => { if (category.items) items += category.items.length if (category.subCategories) { category.subCategories.forEach((sub: any) => { if (sub.items) items += sub.items.length }) } }) const size = new Blob([jsonString]).size setDataStats({ categories, items, size }) } return true } catch (error) { setIsJsonValid(false) setJsonError((error as Error).message) return false } } // 加载当前导航数据 const loadNavigationData = async () => { setIsLoading(true) try { const response = await fetch('/api/navigation') if (response.ok) { const data = await response.json() const jsonString = JSON.stringify(data, null, 2) setNavigationData(jsonString) validateJson(jsonString) } else { throw new Error('加载数据失败') } } catch (error) { toast({ title: "错误", description: "加载导航数据失败", variant: "destructive", }) } finally { setIsLoading(false) } } // 恢复初始化数据 const restoreDefaultData = async () => { // 检查默认文件是否存在 if (!defaultFileStatus.exists) { toast({ title: "错误", description: "navigation-default.json 文件不存在,无法恢复默认数据", variant: "destructive", }) return } if (!defaultFileStatus.valid) { toast({ title: "错误", description: "navigation-default.json 文件格式无效,无法恢复默认数据", variant: "destructive", }) return } setIsLoading(true) try { const response = await fetch('/api/navigation/restore', { method: 'POST', }) if (response.ok) { const data = await response.json() const jsonString = JSON.stringify(data, null, 2) setNavigationData(jsonString) validateJson(jsonString) setIsRestoreDialogOpen(false) toast({ title: "成功", description: `已恢复为初始化数据(${defaultFileStatus.itemCount} 个分类)`, }) } else { const errorData = await response.json() throw new Error(errorData.details || '恢复数据失败') } } catch (error) { toast({ title: "错误", description: (error as Error).message || "恢复初始化数据失败", variant: "destructive", }) } finally { setIsLoading(false) } } // 保存数据 const saveData = async () => { if (!validateJson(navigationData)) { toast({ title: "错误", description: "JSON格式不正确,请检查语法", variant: "destructive", }) return } setIsSaving(true) try { const response = await fetch('/api/navigation', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: navigationData, }) if (response.ok) { toast({ title: "成功", description: "数据保存成功", }) } else { throw new Error('保存失败') } } catch (error) { toast({ title: "错误", description: "保存数据失败", variant: "destructive", }) } finally { setIsSaving(false) } } // 下载数据到本地 const downloadData = () => { if (!validateJson(navigationData)) { toast({ title: "错误", description: "JSON格式不正确,无法下载", variant: "destructive", }) return } try { const parsedData = JSON.parse(navigationData) const blob = new Blob([JSON.stringify(parsedData, null, 2)], { type: 'application/json', }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `navigation-${new Date().toISOString().split('T')[0]}.json` document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) toast({ title: "成功", description: "数据已下载到本地", }) } catch (error) { toast({ title: "错误", description: "下载失败", variant: "destructive", }) } } // 上传JSON文件 const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (!file) return // 检查文件类型 if (!file.name.endsWith('.json') && file.type !== 'application/json') { toast({ title: "错误", description: "请选择JSON文件", variant: "destructive", }) return } // 检查文件大小 (限制为5MB) if (file.size > 5 * 1024 * 1024) { toast({ title: "错误", description: "文件大小不能超过5MB", variant: "destructive", }) return } const reader = new FileReader() reader.onload = (e) => { try { const content = e.target?.result as string // 验证JSON格式 JSON.parse(content) // 格式化JSON并设置到编辑器 const formatted = JSON.stringify(JSON.parse(content), null, 2) setNavigationData(formatted) validateJson(formatted) toast({ title: "成功", description: `已上传文件: ${file.name}`, }) } catch (error) { toast({ title: "错误", description: "文件内容不是有效的JSON格式", variant: "destructive", }) } } reader.onerror = () => { toast({ title: "错误", description: "文件读取失败", variant: "destructive", }) } reader.readAsText(file) // 清空input值,允许重复上传同一文件 event.target.value = '' } // 触发文件选择 const triggerFileUpload = () => { const input = document.createElement('input') input.type = 'file' input.accept = '.json,application/json' input.onchange = (e) => handleFileUpload(e as any) input.click() } // 处理编辑器变化 const handleEditorChange = (value: string) => { setNavigationData(value) validateJson(value) } // 处理编辑器验证 const handleEditorValidate = (isValid: boolean, errors: string[]) => { setIsJsonValid(isValid) setEditorErrors(errors) if (!isValid && errors.length > 0) { setJsonError(errors[0]) } else { setJsonError('') } } useEffect(() => { loadNavigationData() checkDefaultFile() }, []) // Monaco Editor 事件监听 useEffect(() => { const handleMonacoSave = () => { if (!isSaving && !isLoading && isJsonValid) { saveData() } } const handleMonacoRefresh = () => { if (!isLoading) { loadNavigationData() } } const handleMonacoDownload = () => { if (!isLoading && isJsonValid) { downloadData() } } window.addEventListener('monaco-save', handleMonacoSave) window.addEventListener('monaco-refresh', handleMonacoRefresh) window.addEventListener('monaco-download', handleMonacoDownload) return () => { window.removeEventListener('monaco-save', handleMonacoSave) window.removeEventListener('monaco-refresh', handleMonacoRefresh) window.removeEventListener('monaco-download', handleMonacoDownload) } }, [isSaving, isLoading, isJsonValid, navigationData]) return (
{/* 页面标题 */}

数据管理

管理导航数据,支持恢复初始化、在线编辑和数据下载

{/* 操作面板 */}
确认恢复初始化数据?
此操作将使用 navigation-default.json 的数据覆盖当前的 navigation.json 文件。
{/* 默认文件状态 */}
默认文件状态:
文件存在: {defaultFileStatus.exists ? "是" : "否"}
格式有效: {defaultFileStatus.valid ? "是" : "否"}
{defaultFileStatus.exists && defaultFileStatus.valid && (
分类数量: {defaultFileStatus.itemCount}
)}
⚠️ 这个操作不可撤销,请确认是否继续。
{/* JSON 编辑器 */} {/* 错误提示 */} {!isJsonValid && jsonError && (
JSON 语法错误

{jsonError}

)}
{/* 加载遮罩 */} {isLoading && (
加载数据中...
)}
{/* 数据预览和统计 */}
{/* 快速统计 */}
字符数: {navigationData.length.toLocaleString()} 行数: {navigationData.split('\n').length} 大小: {(new Blob([navigationData]).size / 1024).toFixed(2)} KB
{isJsonValid ? (
格式正确
) : (
格式错误
)}
{/* 数据结构预览 */} {isJsonValid && navigationData && (
数据结构
{(() => { try { const data = JSON.parse(navigationData) return (
根分类: {data.navigationItems?.length || 0}
{data.navigationItems?.map((item: any, index: number) => (
• {item.title} {(item.items?.length || 0) + (item.subCategories?.reduce((acc: number, sub: any) => acc + (sub.items?.length || 0), 0) || 0)} 项
))}
) } catch { return 无法解析数据结构 } })()}
最近修改
最后保存: 未保存
数据版本: 当前
备份状态: 可恢复
)}
) }