Fix SSR hydration issues and finalize deployment config
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
@@ -1629,6 +1630,18 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pg": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"pg-protocol": "*",
|
||||||
|
"pg-types": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
|
|||||||
331
src/app/page.tsx
331
src/app/page.tsx
@@ -507,331 +507,12 @@ function HomeInner() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const [activeSection, setActiveSection] = useState("");
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
const currentDoc = documentation.find((doc) => doc.slug === activeDoc);
|
function Home() {
|
||||||
const sections = currentDoc?.sections || [];
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
const handleCopy = () => {
|
if (!mounted) return <div className="min-h-screen bg-zinc-50 dark:bg-zinc-900"></div>;
|
||||||
navigator.clipboard.writeText("npm install docs-site");
|
return <HomeInner />;
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNavClick = (id: string) => {
|
|
||||||
setActiveSection(id);
|
|
||||||
setSidebarOpen(false);
|
|
||||||
const element = document.getElementById(id);
|
|
||||||
element?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredDocs = documentation.filter(
|
|
||||||
(doc) =>
|
|
||||||
doc.title.includes(searchQuery) ||
|
|
||||||
doc.description.includes(searchQuery)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${darkMode ? "dark" : ""} h-full`}>
|
|
||||||
<div className="flex h-full min-h-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100 transition-colors duration-200">
|
|
||||||
{/* 移动端遮罩 */}
|
|
||||||
{sidebarOpen && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
|
||||||
onClick={() => setSidebarOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 左侧导航栏 */}
|
|
||||||
<aside
|
|
||||||
className={`fixed lg:sticky top-0 left-0 z-50 h-screen w-72 bg-white dark:bg-zinc-800 border-r border-zinc-200 dark:border-zinc-700 transition-transform duration-300 ${
|
|
||||||
sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="p-6 h-full flex flex-col">
|
|
||||||
{/* Logo */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-violet-500 to-fuchsia-500 rounded-lg flex items-center justify-center">
|
|
||||||
<span className="text-white font-bold text-lg">D</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-xl font-bold">DocsSite</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 搜索 */}
|
|
||||||
<div className="mb-6 relative">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="搜索文档..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full pl-10 pr-4 py-2 bg-zinc-100 dark:bg-zinc-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
|
|
||||||
/>
|
|
||||||
<svg
|
|
||||||
className="absolute left-3 top-2.5 w-4 h-4 text-zinc-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 文档列表 */}
|
|
||||||
<nav className="flex-1 overflow-y-auto">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{filteredDocs.map((doc) => (
|
|
||||||
<button
|
|
||||||
key={doc.slug}
|
|
||||||
onClick={() => {
|
|
||||||
setActiveDoc(doc.slug);
|
|
||||||
setSidebarOpen(false);
|
|
||||||
}}
|
|
||||||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
|
|
||||||
activeDoc === doc.slug
|
|
||||||
? "bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300"
|
|
||||||
: "hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="font-medium text-sm">{doc.title}</div>
|
|
||||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
|
||||||
{doc.description}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* 主内容区 */}
|
|
||||||
<main className="flex-1 min-w-0">
|
|
||||||
{/* 顶部导航栏 */}
|
|
||||||
<header className="sticky top-0 z-30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-700">
|
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
||||||
className="lg:hidden p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-6 h-6"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<h2 className="text-xl font-semibold">{currentDoc?.title}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{!authenticated && (
|
|
||||||
<a
|
|
||||||
href="/admin"
|
|
||||||
className="px-3 py-1.5 bg-violet-500 hover:bg-violet-600 text-white text-sm rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
登录
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{/* 深色模式切换 */}
|
|
||||||
<button
|
|
||||||
onClick={toggleTheme}
|
|
||||||
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
|
|
||||||
title={theme === "light" ? "切换到深色模式" : "切换到浅色模式"}
|
|
||||||
>
|
|
||||||
{theme === "light" ? (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 复制链接 */}
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors relative"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{copied && (
|
|
||||||
<span className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full text-white text-xs flex items-center justify-center">
|
|
||||||
✓
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* 文档内容 */}
|
|
||||||
<div className="max-w-4xl mx-auto px-6 py-8">
|
|
||||||
{sections.map((section, index) => (
|
|
||||||
<section
|
|
||||||
key={section.title}
|
|
||||||
id={section.title}
|
|
||||||
className={`mb-12 scroll-mt-24`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-2xl font-semibold">{section.title}</h3>
|
|
||||||
<a
|
|
||||||
href={`#${section.title}`}
|
|
||||||
className="opacity-0 hover:opacity-100 transition-opacity text-zinc-400 hover:text-violet-500"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{section.content && (
|
|
||||||
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed">
|
|
||||||
{section.content}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{section.features && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{(section.features as string[] | FeatureItem[]).map((feature, i) => {
|
|
||||||
if (typeof feature === "string") {
|
|
||||||
return (
|
|
||||||
<div key={i} className="flex items-start gap-3">
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span>{feature}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FeatureCard key={i} title={feature.title} description={feature.description}>
|
|
||||||
{feature.items && (
|
|
||||||
<ul className="space-y-2 mt-2">
|
|
||||||
{feature.items.map((item, j) => (
|
|
||||||
<li key={j} className="flex items-start gap-2 text-sm">
|
|
||||||
<span className="text-green-500 mt-0.5">✓</span>
|
|
||||||
{item}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</FeatureCard>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{section.code && (
|
|
||||||
<CodeBlock language="bash">
|
|
||||||
{section.code}
|
|
||||||
</CodeBlock>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{section.instructions && section.code && (
|
|
||||||
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed mt-4">
|
|
||||||
{section.instructions}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 便签 */}
|
|
||||||
<div className="py-8">
|
|
||||||
<NotesList userId={auth.user?.id || null} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 留言板 */}
|
|
||||||
<div className="py-8 border-t border-zinc-200 dark:border-zinc-700">
|
|
||||||
<MessageBoard userId={auth.user?.id || null} username={auth.user?.username || null} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部 */}
|
|
||||||
<footer className="border-t border-zinc-200 dark:border-zinc-700 py-6 px-6">
|
|
||||||
<div className="max-w-4xl mx-auto flex items-center justify-between">
|
|
||||||
<p className="text-sm text-zinc-500">
|
|
||||||
© 2026 DocsSite. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
|
|
||||||
>
|
|
||||||
隐私政策
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
|
|
||||||
>
|
|
||||||
联系方式
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomeInner;
|
export default Home;
|
||||||
|
|||||||
517
src/app/page.tsx.backup
Normal file
517
src/app/page.tsx.backup
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import CodeBlock from "@/components/CodeBlock";
|
||||||
|
import NotesList from "@/components/note/NotesList";
|
||||||
|
import MessageBoard from "@/components/message/MessageBoard";
|
||||||
|
import { useTheme } from "@/components/ThemeProvider";
|
||||||
|
import { useAuth } from "@/components/AuthProvider";
|
||||||
|
|
||||||
|
const documentation = [
|
||||||
|
{
|
||||||
|
title: "快速开始",
|
||||||
|
slug: "quick-start",
|
||||||
|
description: "了解如何开始使用我们的平台",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: "简介",
|
||||||
|
content: "欢迎使用我们的在线文档系统!这是一个现代、响应式、功能丰富的文档平台,专为技术文档设计。",
|
||||||
|
features: [
|
||||||
|
"响应式布局,支持移动端和桌面端",
|
||||||
|
"平滑滚动和锚点导航",
|
||||||
|
"代码高亮和复制功能",
|
||||||
|
"深色模式支持",
|
||||||
|
"内置搜索功能",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "安装",
|
||||||
|
content: "开始之前,请确保你已经安装了以下工具:",
|
||||||
|
requirements: ["Node.js (v18 或更高版本)", "npm 或 yarn 包管理器"],
|
||||||
|
code: "npm install\ngit pull origin main",
|
||||||
|
instructions: "然后运行以下命令:\n\nnpm install\n\n然后:\n\nnpm run dev\n\n访问 http://localhost:3000 即可查看。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "项目结构",
|
||||||
|
content: "项目采用以下目录结构:",
|
||||||
|
code: `docs-site/
|
||||||
|
├── src/
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── page.tsx # 主文档页面
|
||||||
|
│ │ ├── layout.tsx # 根布局
|
||||||
|
│ │ └── globals.css # 全局样式
|
||||||
|
│ └── components/
|
||||||
|
│ ├── Sidebar.tsx # 侧边栏组件
|
||||||
|
│ ├── Navbar.tsx # 导航栏组件
|
||||||
|
│ └── CodeBlock.tsx # 代码块组件
|
||||||
|
├── public/
|
||||||
|
└── package.json`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "核心功能",
|
||||||
|
slug: "features",
|
||||||
|
description: "探索平台的各项功能",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: "响应式布局",
|
||||||
|
content: "平台采用响应式设计,自动适配各种屏幕尺寸。",
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
title: "移动端",
|
||||||
|
items: ["侧边栏可折叠", "字体大小自动调整", "导航栏简化"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "桌面端",
|
||||||
|
items: ["固定侧边栏", "更多内容展示", "优化排版"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "交互功能",
|
||||||
|
content: "平台提供丰富的交互功能:",
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
title: "代码复制",
|
||||||
|
description: "点击代码块右上角的复制按钮即可快速复制代码",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "平滑滚动",
|
||||||
|
description: "点击导航链接自动滚动到对应位置",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "深色模式",
|
||||||
|
description: "点击右上角切换按钮即可切换主题",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "锚点导航",
|
||||||
|
description: "每个章节都有独立的锚点链接",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "搜索功能",
|
||||||
|
content: "内置搜索功能帮助你快速找到所需内容。",
|
||||||
|
code: "1. 点击右上角的搜索图标\n2. 输入关键词\n3. 查看搜索结果\n4. 点击结果跳转到对应章节",
|
||||||
|
instructions: "搜索支持标题、描述和代码片段。",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "API 参考",
|
||||||
|
slug: "api-reference",
|
||||||
|
description: "API 接口文档",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: "获取文档列表",
|
||||||
|
content: "获取所有可用文档的列表。",
|
||||||
|
code: `// GET /api/docs
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"docs": [
|
||||||
|
{
|
||||||
|
"title": "文档标题",
|
||||||
|
"slug": "文档_slug",
|
||||||
|
"description": "文档描述"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "获取文档内容",
|
||||||
|
content: "获取指定文档的详细内容。",
|
||||||
|
code: `// GET /api/docs/:slug
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"title": "文档标题",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "章节标题",
|
||||||
|
"content": "章节内容"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface FeatureItem {
|
||||||
|
title: string;
|
||||||
|
items?: string[];
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeatureCard({ title, description, children }: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="bg-zinc-100 dark:bg-zinc-800 rounded-lg p-4 mb-4 hover:shadow-lg transition-shadow">
|
||||||
|
<h4 className="font-semibold text-lg mb-2">{title}</h4>
|
||||||
|
{description && <p className="text-zinc-600 dark:text-zinc-400">{description}</p>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HomeInner() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
const { authenticated, user, checkAuth } = useAuth();
|
||||||
|
const [activeDoc, setActiveDoc] = useState("quick-start");
|
||||||
|
const [activeSection, setActiveSection] = useState("");
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const currentDoc = documentation.find((doc) => doc.slug === activeDoc);
|
||||||
|
const sections = currentDoc?.sections || [];
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText("npm install docs-site");
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavClick = (id: string) => {
|
||||||
|
setActiveSection(id);
|
||||||
|
setSidebarOpen(false);
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
element?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredDocs = documentation.filter(
|
||||||
|
(doc) =>
|
||||||
|
doc.title.includes(searchQuery) ||
|
||||||
|
doc.description.includes(searchQuery)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${theme} h-full`}>
|
||||||
|
<div className="flex h-full min-h-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100 transition-colors duration-200">
|
||||||
|
{/* 移动端遮罩 */}
|
||||||
|
{sidebarOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 左侧导航栏 */}
|
||||||
|
<aside
|
||||||
|
className={`fixed lg:sticky top-0 left-0 z-50 h-screen w-72 bg-white dark:bg-zinc-800 border-r border-zinc-200 dark:border-zinc-700 transition-transform duration-300 ${
|
||||||
|
sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-6 h-full flex flex-col">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-violet-500 to-fuchsia-500 rounded-lg flex items-center justify-center">
|
||||||
|
<span className="text-white font-bold text-lg">D</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-bold">DocsSite</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 搜索 */}
|
||||||
|
<div className="mb-6 relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索文档..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 bg-zinc-100 dark:bg-zinc-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
className="absolute left-3 top-2.5 w-4 h-4 text-zinc-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文档列表 */}
|
||||||
|
<nav className="flex-1 overflow-y-auto">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{filteredDocs.map((doc) => (
|
||||||
|
<button
|
||||||
|
key={doc.slug}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveDoc(doc.slug);
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-3 py-2 rounded-lg transition-colors ${
|
||||||
|
activeDoc === doc.slug
|
||||||
|
? "bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300"
|
||||||
|
: "hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="font-medium text-sm">{doc.title}</div>
|
||||||
|
<div className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||||
|
{doc.description}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* 主内容区 */}
|
||||||
|
<main className="flex-1 min-w-0">
|
||||||
|
{/* 顶部导航栏 */}
|
||||||
|
<header className="sticky top-0 z-30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-700">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
className="lg:hidden p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h2 className="text-xl font-semibold">{currentDoc?.title}</h2>
|
||||||
|
{!authenticated && (
|
||||||
|
<a
|
||||||
|
href="/admin"
|
||||||
|
className="px-3 py-1.5 bg-violet-500 hover:bg-violet-600 text-white text-sm rounded-lg transition-colors sm:hidden"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{authenticated && user && (
|
||||||
|
<div className="hidden sm:flex items-center gap-2 ml-auto">
|
||||||
|
<span className="text-sm text-zinc-500">{user.username}</span>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await fetch("/api/auth/logout", { method: "POST" });
|
||||||
|
await checkAuth();
|
||||||
|
}}
|
||||||
|
className="text-sm text-violet-500 hover:underline"
|
||||||
|
>
|
||||||
|
退出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 深色模式切换 */}
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
|
||||||
|
title={theme === "light" ? "切换到深色模式" : "切换到浅色模式"}
|
||||||
|
>
|
||||||
|
{theme === "light" ? (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 复制链接 */}
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors relative"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{copied && (
|
||||||
|
<span className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full text-white text-xs flex items-center justify-center">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 文档内容 */}
|
||||||
|
<div className="max-w-4xl mx-auto px-6 py-8">
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<section
|
||||||
|
key={section.title}
|
||||||
|
id={section.title}
|
||||||
|
className={`mb-12 scroll-mt-24`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-2xl font-semibold">{section.title}</h3>
|
||||||
|
<a
|
||||||
|
href={`#${section.title}`}
|
||||||
|
className="opacity-0 hover:opacity-100 transition-opacity text-zinc-400 hover:text-violet-500"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{section.content && (
|
||||||
|
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed">
|
||||||
|
{section.content}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.features && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{(section.features as string[] | FeatureItem[]).map((feature, i) => {
|
||||||
|
if (typeof feature === "string") {
|
||||||
|
return (
|
||||||
|
<div key={i} className="flex items-start gap-3">
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{feature}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FeatureCard key={i} title={feature.title} description={feature.description}>
|
||||||
|
{feature.items && (
|
||||||
|
<ul className="space-y-2 mt-2">
|
||||||
|
{feature.items.map((item, j) => (
|
||||||
|
<li key={j} className="flex items-start gap-2 text-sm">
|
||||||
|
<span className="text-green-500 mt-0.5">✓</span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</FeatureCard>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.code && (
|
||||||
|
<CodeBlock language="bash">
|
||||||
|
{section.code}
|
||||||
|
</CodeBlock>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.instructions && section.code && (
|
||||||
|
<p className="text-zinc-700 dark:text-zinc-300 mb-4 leading-relaxed mt-4">
|
||||||
|
{section.instructions}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 便签 */}
|
||||||
|
<div className="py-8">
|
||||||
|
<NotesList userId={user?.id || null} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 留言板 */}
|
||||||
|
<div className="py-8 border-t border-zinc-200 dark:border-zinc-700">
|
||||||
|
<MessageBoard />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部 */}
|
||||||
|
<footer className="border-t border-zinc-200 dark:border-zinc-700 py-6 px-6">
|
||||||
|
<div className="max-w-4xl mx-auto flex items-center justify-between">
|
||||||
|
<p className="text-sm text-zinc-500">
|
||||||
|
© 2026 DocsSite. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
|
||||||
|
>
|
||||||
|
隐私政策
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-sm text-zinc-500 hover:text-violet-500 transition-colors"
|
||||||
|
>
|
||||||
|
联系方式
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
if (!mounted) return <div className="min-h-screen bg-zinc-50 dark:bg-zinc-900"></div>;
|
||||||
|
return <HomeInner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
@@ -42,13 +42,9 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
{children}
|
{mounted ? children : <div>{children}</div>}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ export async function getClient(): Promise<PoolClient> {
|
|||||||
const client = await getPool().connect();
|
const client = await getPool().connect();
|
||||||
const release = () => client.release();
|
const release = () => client.release();
|
||||||
|
|
||||||
return {
|
return client as PoolClient & { release: () => void };
|
||||||
...client,
|
|
||||||
release,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据库表
|
// 初始化数据库表
|
||||||
@@ -213,7 +210,7 @@ export async function getDocs(): Promise<any[]> {
|
|||||||
ORDER BY d.created_at DESC
|
ORDER BY d.created_at DESC
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return result.rows.map((row) => ({
|
return result.rows.map((row: any) => ({
|
||||||
title: row.title,
|
title: row.title,
|
||||||
slug: row.slug,
|
slug: row.slug,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
|||||||
Reference in New Issue
Block a user