首次发布git

This commit is contained in:
2026-02-24 09:53:19 +08:00
commit dd97cf6a2c
71 changed files with 14875 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
"use client";
import React, { createContext, useContext, useRef, useState, useCallback } from "react";
import { useEditorStore } from "@/lib/store";
import { marked } from "marked";
import JSZip from "jszip";
interface ImportContextType {
triggerImport: (parentId?: string | null) => void;
isImporting: boolean;
}
const ImportContext = createContext<ImportContextType | undefined>(undefined);
export function ImportProvider({ children }: { children: React.ReactNode }) {
const [isImporting, setIsImporting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const targetParentIdRef = useRef<string | null>(null);
const { fetchPages } = useEditorStore();
const triggerImport = useCallback((parentId: string | null = null) => {
targetParentIdRef.current = parentId;
if (fileInputRef.current) {
fileInputRef.current.value = ''; // Reset
fileInputRef.current.click();
}
}, []);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsImporting(true);
try {
if (file.name.toLowerCase().endsWith('.zip')) {
await handleZipImport(file);
} else {
await handleSingleFileImport(file);
}
} catch (error) {
console.error("Import failed", error);
alert("导入失败,请检查文件");
} finally {
setIsImporting(false);
fetchPages();
}
};
const handleSingleFileImport = async (file: File) => {
const text = await file.text();
const title = file.name.replace(/\.md$/i, '');
const html = await marked.parse(text);
await fetch('/api/pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
content: html,
parentId: targetParentIdRef.current,
type: 'file'
}),
});
};
const handleZipImport = async (file: File) => {
const zip = await JSZip.loadAsync(file);
// Create root folder based on zip name
// Remove .md.zip or .zip
const rootName = file.name.replace(/(\.md)?\.zip$/i, '');
const rootRes = await fetch('/api/pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: rootName,
parentId: targetParentIdRef.current,
type: 'folder'
}),
});
if (!rootRes.ok) throw new Error("Failed to create root folder");
const rootFolder = await rootRes.json();
const rootId = rootFolder.id;
// Collect all file entries and infer folder structure
const fileEntries: { path: string, file: JSZip.JSZipObject }[] = [];
const folderPaths = new Set<string>();
zip.forEach((relativePath, zipEntry) => {
if (relativePath.startsWith('__MACOSX') || relativePath.includes('/.') || relativePath.startsWith('.')) return; // Skip hidden/mac files
if (zipEntry.dir) {
// Remove trailing slash
const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
if (cleanPath) folderPaths.add(cleanPath);
} else {
fileEntries.push({ path: relativePath, file: zipEntry });
// Also infer parent folders for files
const parts = relativePath.split('/');
let currentPath = '';
for (let i = 0; i < parts.length - 1; i++) {
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
folderPaths.add(currentPath);
}
}
});
// Map path (e.g. "folder/sub") to database ID. Empty key '' maps to rootId.
const pathMap = new Map<string, string>();
pathMap.set('', rootId);
// Sort folders by depth (length of path splits) to create parents before children
const sortedFolders = Array.from(folderPaths).sort((a, b) => {
return a.split('/').length - b.split('/').length;
});
// Create folders sequentially
for (const folderPath of sortedFolders) {
const parts = folderPath.split('/');
const name = parts[parts.length - 1];
const parentPath = parts.slice(0, -1).join('/');
const parentId = pathMap.get(parentPath); // Should exist because we sorted by depth
if (!parentId) {
console.warn(`Parent not found for ${folderPath}, skipping`);
continue;
}
// Check if we need to create it (users might have zip with folder/ and folder/file, avoiding dups)
// But we didn't check if it existed on server. We assume new import.
// Actually, we are just building map here.
const res = await fetch('/api/pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: name,
parentId: parentId,
type: 'folder'
}),
});
if (res.ok) {
const folder = await res.json();
pathMap.set(folderPath, folder.id);
}
}
// Create files parallel-ish or sequential? Sequential is safer for order but parallel faster.
// Let's do batch sequential to avoid overwhelming server if huge
for (const { path, file } of fileEntries) {
const parts = path.split('/');
const fileName = parts.pop() || "";
const parentPath = parts.join('/');
const parentId = pathMap.get(parentPath);
if (!parentId) continue;
if (!fileName.endsWith('.md') && !fileName.endsWith('.txt')) continue; // Verify extension again
const contentMsg = await file.async('string');
const title = fileName.replace(/\.md$/i, '').replace(/\.txt$/i, '');
const html = await marked.parse(contentMsg);
await fetch('/api/pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
content: html,
parentId: parentId,
type: 'file'
}),
});
}
};
return (
<ImportContext.Provider value={{ triggerImport, isImporting }}>
{children}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept=".md,.txt,.zip"
/>
</ImportContext.Provider>
);
}
export function useImport() {
const context = useContext(ImportContext);
if (!context) {
throw new Error("useImport must be used within an ImportProvider");
}
return context;
}