Files
new-api/web/daisy/src/pages/docs/DocCenter.tsx
T
admin e83ec743c8
Docker Build / Build and Push Docker Image (push) Failing after 1m35s
feat: add DaisyUI frontend theme and document management system
2026-06-13 01:36:06 +08:00

192 lines
7.4 KiB
TypeScript
Vendored

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query'
import { BookOpen, ChevronRight, ChevronDown, Plus, Settings, Menu } from 'lucide-react'
import PageHeader from '@/components/common/PageHeader'
import SearchInput from '@/components/common/SearchInput'
import LoadingSpinner from '@/components/common/LoadingSpinner'
import { usePermission } from '@/hooks/usePermission'
import { getCategories, getDocs } from '@/api/doc'
import { formatDate } from '@/lib/utils'
import { cn } from '@/lib/utils'
import DocCategoryManager from './DocCategoryManager'
export default function DocCenter() {
const { t } = useTranslation()
const navigate = useNavigate()
const { isAdmin } = usePermission()
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [search, setSearch] = useState('')
const [expandedCats, setExpandedCats] = useState<Set<string>>(new Set())
const [drawerOpen, setDrawerOpen] = useState(false)
const [categoryManagerOpen, setCategoryManagerOpen] = useState(false)
const { data: categories = [], isLoading: catLoading } = useQuery({
queryKey: ['doc-categories'],
queryFn: getCategories,
})
const { data: docsData, isLoading: docsLoading } = useQuery({
queryKey: ['docs', selectedCategory, search],
queryFn: () => getDocs({ category: selectedCategory || undefined, search: search || undefined }),
})
const toggleExpand = (name: string) => {
setExpandedCats((prev) => {
const next = new Set(prev)
if (next.has(name)) next.delete(name)
else next.add(name)
return next
})
}
const docs = docsData?.data ?? []
const topCategories = categories.filter((c) => !c.parent_id)
const getChildren = (parentId: number) => categories.filter((c) => c.parent_id === parentId)
return (
<div className="flex flex-col h-full">
<PageHeader
title={t('doc.title')}
description={t('doc.description')}
actions={
isAdmin ? (
<div className="flex gap-2">
<button className="btn btn-sm btn-primary" onClick={() => navigate('/docs/new/edit')}>
<Plus size={16} /> {t('doc.createDoc')}
</button>
<button className="btn btn-sm btn-ghost" onClick={() => setCategoryManagerOpen(true)}>
<Settings size={16} /> {t('doc.manageCategories')}
</button>
</div>
) : undefined
}
/>
<div className="flex flex-1 gap-4 min-h-0">
{/* Mobile drawer toggle */}
<button
className="btn btn-sm btn-ghost lg:hidden fixed top-20 left-4 z-40"
onClick={() => setDrawerOpen(!drawerOpen)}
>
<Menu size={18} />
</button>
{/* Sidebar */}
<aside
className={cn(
'w-64 shrink-0 bg-base-200 rounded-box p-4 overflow-y-auto',
'fixed inset-y-0 left-0 z-30 transition-transform lg:relative lg:translate-x-0',
drawerOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
)}
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-sm">{t('doc.categories')}</h3>
<button className="btn btn-xs btn-ghost lg:hidden" onClick={() => setDrawerOpen(false)}>
{t('common.close')}
</button>
</div>
{catLoading ? (
<LoadingSpinner size="sm" />
) : (
<ul className="menu menu-sm gap-0.5">
<li>
<button
className={cn(!selectedCategory && 'active')}
onClick={() => { setSelectedCategory(null); setDrawerOpen(false) }}
>
<BookOpen size={14} /> {t('doc.allDocs')}
</button>
</li>
{topCategories.map((cat) => {
const children = getChildren(cat.id)
const expanded = expandedCats.has(cat.name)
return (
<li key={cat.id}>
<button
className={cn(selectedCategory === cat.name && 'active')}
onClick={() => { setSelectedCategory(cat.name); setDrawerOpen(false) }}
>
{children.length > 0 ? (
<span className="cursor-pointer" onClick={(e) => { e.stopPropagation(); toggleExpand(cat.name) }}>
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
) : (
<BookOpen size={14} />
)}
{cat.label}
</button>
{expanded && children.length > 0 && (
<ul>
{children.map((child) => (
<li key={child.id}>
<button
className={cn(selectedCategory === child.name && 'active')}
onClick={() => { setSelectedCategory(child.name); setDrawerOpen(false) }}
>
<BookOpen size={12} /> {child.label}
</button>
</li>
))}
</ul>
)}
</li>
)
})}
</ul>
)}
</aside>
{/* Overlay for mobile drawer */}
{drawerOpen && (
<div className="fixed inset-0 bg-black/30 z-20 lg:hidden" onClick={() => setDrawerOpen(false)} />
)}
{/* Content */}
<main className="flex-1 min-w-0">
<div className="mb-4">
<SearchInput value={search} onChange={setSearch} placeholder={t('doc.searchPlaceholder')} />
</div>
{docsLoading ? (
<LoadingSpinner text={t('common.loading')} />
) : docs.length === 0 ? (
<div className="text-center py-16 text-base-content/50">
<BookOpen size={48} className="mx-auto mb-4 opacity-30" />
<p>{t('common.noData')}</p>
</div>
) : (
<div className="grid gap-3">
{docs.map((doc) => (
<div
key={doc.id}
className="card card-compact bg-base-200 hover:bg-base-300 cursor-pointer transition-colors"
onClick={() => navigate(`/docs/${doc.slug}`)}
>
<div className="card-body">
<h3 className="card-title text-base">{doc.title}</h3>
{doc.excerpt && (
<p className="text-sm text-base-content/60 line-clamp-2">{doc.excerpt}</p>
)}
<div className="flex items-center gap-3 text-xs text-base-content/50 mt-1">
{doc.category && (
<span className="badge badge-sm badge-ghost">{doc.category}</span>
)}
<span>{formatDate(doc.updated_at)}</span>
</div>
</div>
</div>
))}
</div>
)}
</main>
</div>
<DocCategoryManager open={categoryManagerOpen} onClose={() => setCategoryManagerOpen(false)} />
</div>
)
}