Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑

This commit is contained in:
2025-11-13 15:24:54 +08:00
commit 42be39b343
249 changed files with 38843 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
'use client'
import React, { useMemo } from 'react'
import { Node, Handle, Position } from '@xyflow/react'
import { Badge } from '@/components/ui/badge'
import { FileCode, FileX } from 'lucide-react'
import { AdaptiveGraph } from '@/components/features/adaptive-graph'
// 节点数据类型
interface GraphNodeData {
id: string
path: string
fileName: string
fileTypeId: string
fileTypeName: string
summary: string | null
dependencyCount: number
isDeleted: boolean
}
// 组件 Props
interface FileDependencyGraphProps {
nodes: GraphNodeData[]
edges: Array<{ source: string; target: string; label?: string }>
onNodeClick?: (node: GraphNodeData) => void
}
// 自定义节点组件
const CustomNode = ({ data }: { data: GraphNodeData & { isHighlighted?: boolean; isDimmed?: boolean } }) => {
const bgColor = data.isDeleted
? 'bg-red-50 dark:bg-red-950/20'
: data.isHighlighted
? 'bg-blue-50 dark:bg-blue-950/50'
: data.isDimmed
? 'bg-gray-50 dark:bg-gray-900/30'
: 'bg-white dark:bg-gray-800'
const borderColor = data.isDeleted
? 'border-red-300 dark:border-red-700'
: data.isHighlighted
? 'border-blue-500 dark:border-blue-400'
: 'border-gray-200 dark:border-gray-700'
const opacity = data.isDimmed ? 'opacity-40' : 'opacity-100'
return (
<div
className={`px-3 py-2 rounded-lg border-2 shadow-sm transition-all ${bgColor} ${borderColor} ${opacity} min-w-[180px] max-w-[220px]`}
>
{/* Handle 组件用于连接边没有Handle看不见连边!opacity-0用来隐藏卡片上用来连接的小点 */}
<Handle type="target" position={Position.Top} className="!opacity-0" />
<div className="flex items-start gap-2">
{data.isDeleted ? (
<FileX className="h-4 w-4 text-red-500 flex-shrink-0 mt-0.5" />
) : (
<FileCode className="h-4 w-4 text-blue-500 flex-shrink-0 mt-0.5" />
)}
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate" title={data.fileName}>
{data.fileName}
</div>
<div className="text-xs text-muted-foreground truncate" title={data.path}>
{data.path}
</div>
<div className="flex items-center gap-1 mt-1">
<Badge variant="outline" className="text-xs px-1 py-0">
{data.fileTypeName}
</Badge>
{data.dependencyCount > 0 && (
<Badge variant="secondary" className="text-xs px-1 py-0">
{data.dependencyCount}
</Badge>
)}
{data.isDeleted && (
<Badge variant="destructive" className="text-xs px-1 py-0">
</Badge>
)}
</div>
</div>
</div>
{/* Handle 组件用于连接边没有Handle看不见连边!opacity-0用来隐藏卡片上用来连接的小点 */}
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
)
}
// 节点类型定义
const nodeTypes = {
custom: CustomNode,
}
export function FileDependencyGraph({ nodes: rawNodes, edges: rawEdges, onNodeClick }: FileDependencyGraphProps) {
// 构建依赖关系映射
const dependencyMap = useMemo(() => {
const map = new Map<string, Set<string>>()
const reverseDependencyMap = new Map<string, Set<string>>()
rawEdges.forEach((edge) => {
// 正向依赖source 依赖 target
if (!map.has(edge.source)) {
map.set(edge.source, new Set())
}
map.get(edge.source)!.add(edge.target)
// 反向依赖target 被 source 依赖
if (!reverseDependencyMap.has(edge.target)) {
reverseDependencyMap.set(edge.target, new Set())
}
reverseDependencyMap.get(edge.target)!.add(edge.source)
})
return { dependencies: map, reverseDependencies: reverseDependencyMap }
}, [rawEdges])
// 过滤函数:根据搜索查询返回过滤后的节点和高亮节点
const handleFilter = useMemo(
() => (nodes: GraphNodeData[], query: string) => {
if (!query.trim()) {
return {
filteredNodeIds: new Set(nodes.map((n) => n.id)),
highlightedNodeIds: new Set<string>(),
}
}
const lowerQuery = query.toLowerCase()
const matchedNodes = nodes.filter(
(node) =>
node.fileName.toLowerCase().includes(lowerQuery) ||
node.path.toLowerCase().includes(lowerQuery) ||
node.summary?.toLowerCase().includes(lowerQuery)
)
// 包含匹配的节点及其依赖和被依赖的节点
const resultSet = new Set<string>()
matchedNodes.forEach((node) => {
resultSet.add(node.id)
// 添加该节点依赖的节点
const deps = dependencyMap.dependencies.get(node.id)
if (deps) {
deps.forEach((depId) => resultSet.add(depId))
}
// 添加依赖该节点的节点
const reverseDeps = dependencyMap.reverseDependencies.get(node.id)
if (reverseDeps) {
reverseDeps.forEach((depId) => resultSet.add(depId))
}
})
return {
filteredNodeIds: resultSet,
highlightedNodeIds: new Set(matchedNodes.map((n) => n.id)),
}
},
[dependencyMap]
)
// 节点转换函数
const transformNode = (
node: GraphNodeData,
options: { isHighlighted: boolean; isDimmed: boolean }
): Node => ({
id: node.id,
type: 'custom',
data: {
...node,
isHighlighted: options.isHighlighted,
isDimmed: options.isDimmed,
},
position: { x: 0, y: 0 }, // 将由布局算法设置
})
// MiniMap 节点颜色函数
const getNodeColor = (node: Node) => {
const data = node.data as unknown as GraphNodeData & { isHighlighted?: boolean; isDeleted?: boolean }
if (data.isDeleted) return '#fca5a5'
if (data.isHighlighted) return '#60a5fa'
return '#cbd5e1'
}
// 统计信息渲染
const renderStats = (filteredCount: number, totalCount: number, matchedCount: number) => (
<div className="mt-2 text-xs text-muted-foreground">
{filteredCount} / {totalCount}
{matchedCount > 0 && ` (${matchedCount} 个匹配)`}
</div>
)
return (
<AdaptiveGraph
nodes={rawNodes}
edges={rawEdges}
nodeTypes={nodeTypes}
onFilter={handleFilter}
transformNode={transformNode}
getNodeColor={getNodeColor}
onNodeClick={onNodeClick}
searchPlaceholder="搜索文件名、路径或摘要..."
renderStats={renderStats}
className="h-[800px] max-h-[calc(100vh-12rem)]"
reactFlowProps={{
nodesDraggable: true,
nodesConnectable: false,
elementsSelectable: true,
}}
/>
)
}