Files
star-erp/resources/js/Pages/System/Manual/Index.tsx
sky121113 e6cf03b991
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 54s
feat: 實作系統操作手冊模組 (Markdown 渲染與導覽)
2026-02-13 15:51:51 +08:00

147 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Head, Link } from "@inertiajs/react";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
BookOpen,
Search,
Menu,
FileText,
HelpCircle
} from "lucide-react";
import { useState } from "react";
import { ScrollArea } from "@/Components/ui/scroll-area";
import { Input } from "@/Components/ui/input";
import { cn } from "@/lib/utils";
interface Page {
title: string;
slug: string;
}
interface Section {
title: string;
pages: Page[];
}
interface Props {
toc: Section[];
currentSlug: string;
content: string;
}
export default function ManualIndex({ toc, currentSlug, content }: Props) {
const [searchQuery, setSearchQuery] = useState("");
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
// Filter TOC based on search
const filteredToc = toc.map(section => ({
...section,
pages: section.pages.filter(page =>
page.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
section.title.toLowerCase().includes(searchQuery.toLowerCase())
)
})).filter(section => section.pages.length > 0);
return (
<AuthenticatedLayout breadcrumbs={[
{ label: "系統管理", href: "#" },
{ label: "操作手冊", href: route('system.manual.index'), isPage: true }
]}>
<Head title="操作手冊" />
<div className="flex h-[calc(100vh-140px)] bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden m-2 md:m-6">
{/* Sidebar */}
<aside className={cn(
"w-64 border-r border-gray-200 bg-gray-50/50 flex flex-col transition-all duration-300",
!isSidebarOpen && "w-0 opacity-0 overflow-hidden"
)}>
<div className="p-4 border-b border-gray-200 bg-white">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜尋手冊..."
className="pl-9 h-9 bg-gray-50 border-gray-200 focus:bg-white transition-colors"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<ScrollArea className="flex-1">
<div className="p-3 space-y-6">
{filteredToc.map((section, idx) => (
<div key={idx} className="space-y-1">
<h3 className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">
{section.title}
</h3>
<div className="space-y-0.5">
{section.pages.map((page) => (
<Link
key={page.slug}
href={route('system.manual.index', { slug: page.slug })}
className={cn(
"flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md transition-colors",
currentSlug === page.slug
? "bg-primary-50 text-primary-700 shadow-sm border border-primary-100"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
)}
>
<FileText className={cn("h-4 w-4", currentSlug === page.slug ? "text-primary-600" : "text-gray-400")} />
{page.title}
</Link>
))}
</div>
</div>
))}
</div>
</ScrollArea>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col min-w-0 bg-white">
{/* Content Header mobile toggle */}
<div className="h-12 border-b border-gray-100 flex items-center px-4 gap-2">
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 transition-colors"
>
<Menu className="h-5 w-5" />
</button>
<div className="h-4 w-px bg-gray-200 mx-1" />
<BookOpen className="h-4 w-4 text-primary-main" />
<span className="text-sm font-medium text-gray-700"></span>
</div>
<ScrollArea className="flex-1">
<div className="max-w-4xl mx-auto p-6 md:p-12">
<article className="prose prose-slate prose-blue max-w-none
prose-headings:font-bold prose-headings:text-gray-900
prose-p:text-gray-600 prose-p:leading-relaxed
prose-li:text-gray-600
prose-pre:bg-gray-900 prose-pre:rounded-xl
prose-img:rounded-xl prose-img:shadow-lg
prose-td:py-3 prose-td:px-4
prose-th:bg-gray-50 prose-th:text-gray-900 prose-th:font-semibold
prose-table:border prose-table:border-gray-200 prose-table:rounded-lg prose-table:overflow-hidden">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{content}
</ReactMarkdown>
</article>
<div className="mt-16 pt-8 border-t border-gray-100 flex items-center justify-between text-sm text-gray-400">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4" />
<span></span>
</div>
<span>Star ERP v1.0</span>
</div>
</div>
</ScrollArea>
</main>
</div>
</AuthenticatedLayout>
);
}