添加 Next.js Web 应用

- 创建 Next.js + TypeScript + Tailwind CSS Web 应用
- 支持 Markdown 渲染和 GFM 格式
- 支持 Obsidian 风格 wiki 链接 [[Page|Label]]
- 侧边栏导航,按分类组织页面
- 图片资源支持(通过 API 路由从 wiki/assets 提供)
- 响应式设计
- 更新 README 添加 Web 应用使用说明
This commit is contained in:
Junjian Wang
2026-04-13 20:54:14 +08:00
parent a58453865b
commit 3b6dc2e9fc
25 changed files with 8752 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
export async function GET(
_request: Request,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path: pathSegments } = await params;
const assetPath = path.join(process.cwd(), '../wiki/assets', ...pathSegments);
try {
if (!fs.existsSync(assetPath)) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
const fileBuffer = fs.readFileSync(assetPath);
const ext = path.extname(assetPath).toLowerCase();
const contentTypeMap: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp',
};
return new NextResponse(fileBuffer, {
headers: {
'Content-Type': contentTypeMap[ext] || 'application/octet-stream',
},
});
} catch (e) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+80
View File
@@ -0,0 +1,80 @@
@import "tailwindcss";
:root {
--background: #f9fafb;
--foreground: #111827;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
background: var(--background);
color: var(--foreground);
}
.prose-wikillm h1 {
@apply text-3xl font-bold mb-6 mt-8 text-gray-900;
}
.prose-wikillm h2 {
@apply text-2xl font-semibold mb-4 mt-6 text-gray-800;
}
.prose-wikillm h3 {
@apply text-xl font-semibold mb-3 mt-5 text-gray-800;
}
.prose-wikillm p {
@apply mb-4 leading-relaxed;
}
.prose-wikillm ul {
@apply list-disc list-inside mb-4 space-y-1;
}
.prose-wikillm ol {
@apply list-decimal list-inside mb-4 space-y-1;
}
.prose-wikillm li {
@apply leading-relaxed;
}
.prose-wikillm a {
@apply text-blue-600 hover:text-blue-800 hover:underline;
}
.prose-wikillm blockquote {
@apply border-l-4 border-gray-300 pl-4 py-1 my-4 bg-gray-50 italic;
}
.prose-wikillm table {
@apply w-full border-collapse my-6;
}
.prose-wikillm th {
@apply bg-gray-100 border border-gray-300 px-4 py-2 text-left font-semibold;
}
.prose-wikillm td {
@apply border border-gray-300 px-4 py-2;
}
.prose-wikillm code {
@apply bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono;
}
.prose-wikillm pre {
@apply bg-gray-900 text-gray-100 p-4 rounded-lg my-4 overflow-x-auto;
}
.prose-wikillm pre code {
@apply bg-transparent p-0 text-inherit;
}
.prose-wikillm img {
@apply max-w-full h-auto rounded-lg my-4;
}
+19
View File
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "WikiLLM 知识库",
description: "LLM-powered personal knowledge base",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN" className="h-full">
<body className="min-h-full bg-gray-50">{children}</body>
</html>
);
}
+61
View File
@@ -0,0 +1,61 @@
import Link from "next/link";
import { getAllPages } from "@/lib/wiki";
import { Sidebar } from "@/components/Sidebar";
export default async function Home() {
const pages = await getAllPages();
const indexPage = pages.find(p => p.slug === "INDEX");
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<Link href="/" className="text-xl font-bold text-gray-900">
WikiLLM
</Link>
<nav className="flex items-center space-x-4">
<Link
href="/"
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
</Link>
</nav>
</div>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* Sidebar */}
<aside className="lg:col-span-1">
<Sidebar pages={pages} />
</aside>
{/* Main content */}
<main className="lg:col-span-3">
<div className="bg-white rounded-lg shadow-sm p-8">
{indexPage ? (
<div
className="prose-wikillm"
dangerouslySetInnerHTML={{ __html: indexPage.html }}
/>
) : (
<div>
<h1 className="text-3xl font-bold mb-6 text-gray-900">
WikiLLM
</h1>
<p className="text-gray-600 mb-4">
</p>
</div>
)}
</div>
</main>
</div>
</div>
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getAllPages, getPageBySlug, type WikiPage as WikiPageType } from "@/lib/wiki";
import { Sidebar } from "@/components/Sidebar";
export async function generateStaticParams() {
const pages = await getAllPages();
return pages.map((page) => ({
slug: page.slug,
}));
}
export default async function WikiPageComponent({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const page = await getPageBySlug(slug);
if (!page) {
notFound();
}
const allPages = await getAllPages();
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<Link href="/" className="text-xl font-bold text-gray-900">
WikiLLM
</Link>
<nav className="flex items-center space-x-4">
<Link
href="/"
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
</Link>
</nav>
</div>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* Sidebar */}
<aside className="lg:col-span-1">
<Sidebar pages={allPages} currentSlug={slug} />
</aside>
{/* Main content */}
<main className="lg:col-span-3">
<article className="bg-white rounded-lg shadow-sm p-8">
{/* Page header */}
<header className="mb-8 border-b border-gray-200 pb-4">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
{page.title}
</h1>
{page.frontmatter.last_updated && (
<p className="text-sm text-gray-500">
: {String(page.frontmatter.last_updated)}
</p>
)}
{page.frontmatter.tags && page.frontmatter.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{page.frontmatter.tags.map((tag: string) => (
<span
key={tag}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{tag}
</span>
))}
</div>
)}
</header>
{/* Page content */}
<div
className="prose-wikillm"
dangerouslySetInnerHTML={{ __html: page.html }}
/>
</article>
</main>
</div>
</div>
</div>
);
}