diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 06479db..e3f9899 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import { DesignSystemPreview } from '@/features/design-system/DesignSystemPreview' +import { KnowledgeBaseListPage } from '@/features/knowledge/KnowledgeBaseListPage' +import { KnowledgeItemEditorPage } from '@/features/knowledge/KnowledgeItemEditorPage' +import { KnowledgeSearchPage } from '@/features/knowledge/KnowledgeSearchPage' import { SolutionCreatePage } from '@/features/solutions/SolutionCreatePage' import { SolutionListPage } from '@/features/solutions/SolutionListPage' import { AuditLogPage } from '@/features/system/AuditLogPage' @@ -54,6 +57,18 @@ export function Router() { return } + if (route === '/knowledge') { + return + } + + if (route === '/knowledge/search') { + return + } + + if (route === '/knowledge/items/new') { + return + } + if (route === '/solutions') { return } diff --git a/frontend/src/components/app/AppSidebar.tsx b/frontend/src/components/app/AppSidebar.tsx index bd34ca8..389968f 100644 --- a/frontend/src/components/app/AppSidebar.tsx +++ b/frontend/src/components/app/AppSidebar.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Bot, + BookOpen, BriefcaseBusiness, ClipboardList, Database, @@ -8,6 +9,7 @@ import { Library, Menu, ScrollText, + Search, Settings, ShieldCheck, Ticket, @@ -23,6 +25,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh const iconMap = { audit: ScrollText, + book_open: BookOpen, bot: Bot, briefcase: BriefcaseBusiness, database: Database, @@ -30,6 +33,7 @@ const iconMap = { library: Library, menu: Menu, roles: UserCog, + search: Search, settings: Settings, shield: ShieldCheck, ticket: Ticket, @@ -57,6 +61,41 @@ function visibleChildren(menu: AuthMenu) { return menu.children.filter((item) => item.is_visible && item.is_enabled) } +const knowledgeNavGroup: AuthMenu = { + id: -100, + code: 'knowledge_builtin', + title: '知识库', + path: '#/knowledge', + icon: 'book_open', + permission_code: 'knowledge.view', + is_visible: true, + is_enabled: true, + children: [ + { + id: -101, + code: 'knowledge_bases', + title: '知识库', + path: '#/knowledge', + icon: 'book_open', + permission_code: 'knowledge.view', + is_visible: true, + is_enabled: true, + children: [] + }, + { + id: -102, + code: 'knowledge_search', + title: '知识检索', + path: '#/knowledge/search', + icon: 'search', + permission_code: 'knowledge.view', + is_visible: true, + is_enabled: true, + children: [] + } + ] +} + type SidebarNavContentProps = { groups: AuthMenu[] currentHash: string @@ -120,7 +159,11 @@ type AppSidebarProps = { export function AppSidebar({ mobileOpen = false, onMobileOpenChange }: AppSidebarProps) { const { user } = useAuth() const currentHash = useCurrentHash() - const groups = (user?.menus ?? []).filter((menu) => menu.is_visible && menu.is_enabled) + const menuGroups = (user?.menus ?? []).filter((menu) => menu.is_visible && menu.is_enabled) + const hasKnowledgeMenu = menuGroups.some( + (group) => group.code.startsWith('knowledge') || visibleChildren(group).some((item) => item.path.startsWith('#/knowledge')) + ) + const groups = hasKnowledgeMenu ? menuGroups : [...menuGroups, knowledgeNavGroup] return ( <> diff --git a/frontend/src/features/knowledge/KnowledgeBaseListPage.tsx b/frontend/src/features/knowledge/KnowledgeBaseListPage.tsx new file mode 100644 index 0000000..3b2059c --- /dev/null +++ b/frontend/src/features/knowledge/KnowledgeBaseListPage.tsx @@ -0,0 +1,151 @@ +import * as React from 'react' +import { Plus, Search } from 'lucide-react' + +import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable' +import { AppEmptyState } from '@/components/app/AppEmptyState' +import { AppField } from '@/components/app/AppField' +import { AppPage } from '@/components/app/AppPage' +import { AppPageHeader } from '@/components/app/AppPageHeader' +import { AppSection } from '@/components/app/AppSection' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { formatDateTime } from '@/lib/format' + +import { createKnowledgeBase, listKnowledgeBases } from './api' +import type { KnowledgeBase } from './types' + +type KnowledgeBaseForm = { + name: string + description: string + scope: string +} + +const emptyForm: KnowledgeBaseForm = { + name: '', + description: '', + scope: '' +} + +const columns: AppDataTableColumn[] = [ + { + key: 'name', + header: '名称', + cell: (base) => ( +
+

{base.name}

+

+ {base.description || '暂无描述'} +

+
+ ) + }, + { + key: 'scope', + header: '范围', + cell: (base) => {base.scope || '-'} + }, + { + key: 'enabled', + header: '启用状态', + cell: (base) => ( + + {base.enabled ? '已启用' : '已停用'} + + ) + }, + { + key: 'updated_at', + header: '更新时间', + cell: (base) => {formatDateTime(base.updated_at)} + } +] + +export function KnowledgeBaseListPage() { + const [bases, setBases] = React.useState([]) + const [form, setForm] = React.useState(emptyForm) + const [error, setError] = React.useState('') + + async function load() { + try { + setBases(await listKnowledgeBases()) + setError('') + } catch (reason) { + setError(reason instanceof Error ? reason.message : '加载知识库失败') + } + } + + React.useEffect(() => { + void load() + }, []) + + async function handleCreate(event: React.FormEvent) { + event.preventDefault() + await createKnowledgeBase(form) + setForm(emptyForm) + await load() + } + + return ( + + + + + + } + /> + + +
+ + setForm((current) => ({ ...current, name: event.target.value }))} + placeholder="例如:税务采集知识库" + /> + + + setForm((current) => ({ ...current, scope: event.target.value }))} + placeholder="例如:数据采集" + /> + + + setForm((current) => ({ ...current, description: event.target.value }))} + placeholder="知识库用途说明" + /> + + +
+
+ + {error ? : null} + {!error ? ( + base.id} + emptyTitle="暂无知识库" + emptyDescription="创建知识库后,可继续维护文档、知识条目和检索内容。" + footer={

共 {bases.length} 个知识库

} + /> + ) : null} +
+ ) +} diff --git a/frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx b/frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx new file mode 100644 index 0000000..012fdf6 --- /dev/null +++ b/frontend/src/features/knowledge/KnowledgeItemEditorPage.tsx @@ -0,0 +1,202 @@ +import * as React from 'react' +import { Save } from 'lucide-react' + +import { AppEmptyState } from '@/components/app/AppEmptyState' +import { AppField } from '@/components/app/AppField' +import { AppPage } from '@/components/app/AppPage' +import { AppPageHeader } from '@/components/app/AppPageHeader' +import { AppSection } from '@/components/app/AppSection' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' + +import { createKnowledgeItem, listKnowledgeBases } from './api' +import type { KnowledgeBase, KnowledgeItemInput } from './types' + +const initialForm: KnowledgeItemInput = { + knowledge_base_id: 0, + title: '', + problem: '', + applicable_scope: '', + error_code: '', + root_cause: '', + troubleshooting_steps: '', + solution: '', + risk_note: '', + status: 'draft' +} + +export function KnowledgeItemEditorPage() { + const [bases, setBases] = React.useState([]) + const [form, setForm] = React.useState(initialForm) + const [error, setError] = React.useState('') + const [submitting, setSubmitting] = React.useState(false) + + React.useEffect(() => { + listKnowledgeBases() + .then((items) => { + setBases(items) + setForm((current) => ({ + ...current, + knowledge_base_id: current.knowledge_base_id || items[0]?.id || 0 + })) + }) + .catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载知识库失败')) + }, []) + + function updateField(field: Key, value: KnowledgeItemInput[Key]) { + setForm((current) => ({ ...current, [field]: value })) + } + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault() + if (!form.knowledge_base_id) { + setError('请先创建并选择知识库。') + return + } + setSubmitting(true) + try { + await createKnowledgeItem(form) + window.location.hash = '/knowledge/search' + } catch (reason) { + setError(reason instanceof Error ? reason.message : '保存知识条目失败') + } finally { + setSubmitting(false) + } + } + + return ( + + + {error ? : null} + +
+ +
+ + + + + updateField('title', event.target.value)} + placeholder="例如:税局采集进度查询" + /> + + + + +
+
+ + +
+ +