Files
selfrelease 5278190750 feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务
- 后端: 成本分析服务、AI问答服务
- 后端: 科目映射CRUD API、分析API、QA API
- 后端: 集成测试(认证/任务/凭证) 49个测试全部通过
- 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面
- 前端: AuthGuard认证守卫、Dashboard AI聊天功能
- 前端: Playwright E2E测试 16 passed, 1 skipped
- 基础设施: Docker Compose、Nginx反向代理、.env.example
- 文档: 用户手册、管理员手册、发布检查清单
2026-07-07 21:21:29 +08:00

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>
);
}