5278190750
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务 - 后端: 成本分析服务、AI问答服务 - 后端: 科目映射CRUD API、分析API、QA API - 后端: 集成测试(认证/任务/凭证) 49个测试全部通过 - 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面 - 前端: AuthGuard认证守卫、Dashboard AI聊天功能 - 前端: Playwright E2E测试 16 passed, 1 skipped - 基础设施: Docker Compose、Nginx反向代理、.env.example - 文档: 用户手册、管理员手册、发布检查清单
249 lines
8.5 KiB
TypeScript
249 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { motion } from "motion/react";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
BookOpen,
|
|
Search,
|
|
FileText,
|
|
HelpCircle,
|
|
Lightbulb,
|
|
TrendingUp,
|
|
Receipt,
|
|
Upload,
|
|
ArrowRight,
|
|
} from "lucide-react";
|
|
|
|
interface KnowledgeArticle {
|
|
id: number;
|
|
title: string;
|
|
category: string;
|
|
summary: string;
|
|
tags: string[];
|
|
}
|
|
|
|
const categoryConfig: Record<string, { label: string; icon: any; color: string }> = {
|
|
getting_started: { label: "快速入门", icon: Upload, color: "text-blue-500" },
|
|
reconciliation: { label: "对账指南", icon: FileText, color: "text-primary" },
|
|
analysis: { label: "成本分析", icon: TrendingUp, color: "text-violet-500" },
|
|
voucher: { label: "凭证管理", icon: Receipt, color: "text-emerald-500" },
|
|
faq: { label: "常见问题", icon: HelpCircle, color: "text-amber-500" },
|
|
tips: { label: "使用技巧", icon: Lightbulb, color: "text-orange-500" },
|
|
};
|
|
|
|
const mockArticles: KnowledgeArticle[] = [
|
|
{
|
|
id: 1,
|
|
title: "如何上传工资表和社保表",
|
|
category: "getting_started",
|
|
summary: "详细介绍文件上传流程,支持 Excel 格式,包括文件大小限制和格式要求。",
|
|
tags: ["上传", "Excel", "工资表"],
|
|
},
|
|
{
|
|
id: 2,
|
|
title: "AI 字段识别使用指南",
|
|
category: "getting_started",
|
|
summary: "AI 自动识别源字段到标准字段的映射,支持人工确认和规则沉淀。",
|
|
tags: ["AI", "字段映射", "自动识别"],
|
|
},
|
|
{
|
|
id: 3,
|
|
title: "对账规则配置说明",
|
|
category: "reconciliation",
|
|
summary: "了解如何配置对账规则,包括金额容差、必填字段、异常阈值等设置。",
|
|
tags: ["规则", "容差", "配置"],
|
|
},
|
|
{
|
|
id: 4,
|
|
title: "异常处理最佳实践",
|
|
category: "reconciliation",
|
|
summary: "异常分类说明、处理流程建议、批量操作技巧。",
|
|
tags: ["异常", "处理", "批量"],
|
|
},
|
|
{
|
|
id: 5,
|
|
title: "人工成本分析维度说明",
|
|
category: "analysis",
|
|
summary: "总成本、部门拆分、费用科目拆分、环比变化等分析维度详解。",
|
|
tags: ["成本", "分析", "部门"],
|
|
},
|
|
{
|
|
id: 6,
|
|
title: "AI 成本分析摘要解读",
|
|
category: "analysis",
|
|
summary: "AI 生成的成本变化摘要如何理解,如何利用建议追问深入分析。",
|
|
tags: ["AI", "摘要", "环比"],
|
|
},
|
|
{
|
|
id: 7,
|
|
title: "凭证生成与科目映射",
|
|
category: "voucher",
|
|
summary: "科目映射配置方法、凭证自动生成流程、借贷分录预览说明。",
|
|
tags: ["凭证", "科目", "映射"],
|
|
},
|
|
{
|
|
id: 8,
|
|
title: "金蝶 K3 导出格式说明",
|
|
category: "voucher",
|
|
summary: "金蝶 CSV 导出格式字段说明、导入金蝶 K3 的操作步骤。",
|
|
tags: ["金蝶", "导出", "CSV"],
|
|
},
|
|
{
|
|
id: 9,
|
|
title: "AI 问答使用技巧",
|
|
category: "faq",
|
|
summary: "如何有效提问获取准确回答,预置问题 vs 自由提问的使用场景。",
|
|
tags: ["AI", "问答", "技巧"],
|
|
},
|
|
{
|
|
id: 10,
|
|
title: "如何提高对账匹配率",
|
|
category: "tips",
|
|
summary: "通过完善字段映射、调整规则容差、清理异常数据来提升匹配率。",
|
|
tags: ["匹配率", "优化", "技巧"],
|
|
},
|
|
];
|
|
|
|
export default function KnowledgePage() {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [selectedCategory, setSelectedCategory] = useState("");
|
|
|
|
const filteredArticles = mockArticles.filter((article) => {
|
|
const matchesSearch =
|
|
!searchQuery ||
|
|
article.title.includes(searchQuery) ||
|
|
article.summary.includes(searchQuery) ||
|
|
article.tags.some((tag) => tag.includes(searchQuery));
|
|
const matchesCategory = !selectedCategory || article.category === selectedCategory;
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
|
|
return (
|
|
<div className="min-h-full p-6 lg:p-8 max-w-7xl mx-auto space-y-6">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
<h1 className="text-heading-1 text-foreground">企业知识库</h1>
|
|
<p className="text-muted-foreground mt-1">使用指南、最佳实践和常见问题</p>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.1 }}
|
|
>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex gap-3">
|
|
<div className="flex-1 relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="搜索文章、标签..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="h-9 pl-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.15 }}
|
|
className="flex flex-wrap gap-2"
|
|
>
|
|
<Button
|
|
variant={selectedCategory === "" ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setSelectedCategory("")}
|
|
>
|
|
全部
|
|
</Button>
|
|
{Object.entries(categoryConfig).map(([key, config]) => {
|
|
const Icon = config.icon;
|
|
return (
|
|
<Button
|
|
key={key}
|
|
variant={selectedCategory === key ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setSelectedCategory(key)}
|
|
>
|
|
<Icon className="w-4 h-4 mr-1" />
|
|
{config.label}
|
|
</Button>
|
|
);
|
|
})}
|
|
</motion.div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{filteredArticles.map((article, index) => {
|
|
const cat = categoryConfig[article.category];
|
|
const Icon = cat?.icon || BookOpen;
|
|
return (
|
|
<motion.div
|
|
key={article.id}
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3, delay: index * 0.05 }}
|
|
>
|
|
<Card className="group cursor-pointer hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start gap-4">
|
|
<div className="p-2.5 rounded-lg bg-muted group-hover:bg-primary/10 transition-colors">
|
|
<Icon className={`w-5 h-5 ${cat?.color || "text-muted-foreground"}`} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h3 className="font-medium text-foreground group-hover:text-primary transition-colors">
|
|
{article.title}
|
|
</h3>
|
|
</div>
|
|
<p className="text-caption text-muted-foreground mb-3">
|
|
{article.summary}
|
|
</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{article.tags.map((tag) => (
|
|
<Badge key={tag} variant="secondary" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all self-center" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{filteredArticles.length === 0 && (
|
|
<Card>
|
|
<CardContent className="p-12 text-center">
|
|
<div className="w-12 h-12 rounded-full bg-muted mx-auto mb-4 flex items-center justify-center">
|
|
<BookOpen className="w-6 h-6 text-muted-foreground" />
|
|
</div>
|
|
<p className="text-body text-muted-foreground">未找到相关文章</p>
|
|
<p className="text-caption text-muted-foreground mt-1">尝试其他关键词或分类</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|