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,297 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Package, FileCode, Code, FileClock } from "lucide-react";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { SearchInput } from "@/components/common/search-input";
import { ResponsiveTabs, type ResponsiveTabItem } from "@/components/common/responsive-tabs";
import { trpc } from "@/lib/trpc";
import { Skeleton } from "@/components/ui/skeleton";
import { PackageAnalyzeDialog, PackageAnalyzeTrigger } from "./components/PackageAnalyzeDialog";
import { PackageDetailSheet } from "./components/PackageDetailSheet";
import { toast } from 'sonner'
import type { PackageData } from "@/server/routers/dev/arch";
import { formatDate } from "@/lib/format";
// 依赖包卡片组件
function PackageCard({ pkg, onClick }: { pkg: PackageData; onClick: () => void }) {
return (
<Card
className="shadow-sm hover:shadow-md transition-all duration-200 hover:border-primary/20 flex flex-col gap-4 cursor-pointer"
onClick={onClick}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2 min-w-0">
<CardTitle className="flex items-center gap-2 text-xl mb-1.5 min-w-0">
<Package className="size-4.5 shrink-0 text-primary" />
{pkg.homepage ? (
<a
href={pkg.homepage}
target="_blank"
rel="noopener noreferrer"
className="truncate font-semibold bg-gradient-to-r from-purple-600 via-violet-600 to-indigo-600 dark:from-purple-400 dark:via-violet-400 dark:to-indigo-400 bg-clip-text text-transparent hover:opacity-80 transition-opacity"
>
{pkg.name}
</a>
) : (
<span className="truncate font-semibold bg-gradient-to-r from-purple-600 via-violet-600 to-indigo-600 dark:from-purple-400 dark:via-violet-400 dark:to-indigo-400 bg-clip-text text-transparent">
{pkg.name}
</span>
)}
{pkg.repositoryUrl && (
<a
href={pkg.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-primary hover:text-primary/80 transition-colors font-medium"
>
<Code className="size-3" />
</a>
)}
</CardTitle>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="primary" size="sm" appearance="light" className="shrink-0 text-xs cursor-help">
v{pkg.version}
</Badge>
</TooltipTrigger>
<TooltipContent variant="light">
<div className="text-xs">
<div className="font-medium">{pkg.name} v{pkg.version}</div>
<div className="text-muted-foreground"> {formatDate(pkg.modifiedAt)}</div>
</div>
</TooltipContent>
</Tooltip>
</div>
<CardDescription className="line-clamp-3 text-xs leading-snug">
{pkg.description}
</CardDescription>
</CardHeader>
<CardContent className="pt-0 flex flex-col flex-1">
<div className="bg-muted/30 rounded-md p-2.5 mb-3">
<div className="text-xs font-medium text-foreground mb-1 flex items-center gap-1.5">
<span className="size-1 rounded-full bg-primary" />
</div>
<p className="text-xs leading-relaxed text-muted-foreground">{pkg.projectRoleSummary}</p>
</div>
<div className="flex items-center justify-between pt-2 border-t mt-auto">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<FileCode className="size-3 text-primary" />
<span className="font-medium">{pkg.relatedFileCount}</span>
<span></span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 text-xs text-muted-foreground cursor-help">
<FileClock className="size-3" />
<span>{formatDate(pkg.lastAnalyzedAt)}</span>
</div>
</TooltipTrigger>
<TooltipContent variant="light">
<div className="text-xs"></div>
</TooltipContent>
</Tooltip>
</div>
</CardContent>
</Card>
);
}
export default function ArchPackagePage() {
const [searchQuery, setSearchQuery] = useState('');
// 用于刷新数据的 utils
const utils = trpc.useUtils()
// 获取所有包类型
const { data: pkgTypes, isLoading: isLoadingTypes } = trpc.devArch!.getAllPkgTypes.useQuery();
// 获取所有依赖包数据
const { data: packagesByType, isLoading: isLoadingPackages } = trpc.devArch!.getAllPackages.useQuery();
// 刷新依赖包列表
const handleRefreshPackages = useCallback(() => {
utils.devArch!.getAllPackages.invalidate()
utils.devArch!.getAllPkgTypes.invalidate()
}, [utils])
// 使用第一个包类型作为默认激活标签
const [activeTab, setActiveTab] = useState<string>('');
// 分析对话框状态
const [isAnalyzeDialogOpen, setIsAnalyzeDialogOpen] = useState(false)
const [analyzeJobId, setAnalyzeJobId] = useState<string | null>(null)
// 详情Sheet状态
const [selectedPackage, setSelectedPackage] = useState<PackageData | null>(null)
const [isDetailSheetOpen, setIsDetailSheetOpen] = useState(false)
// 处理卡片点击
const handleCardClick = useCallback((pkg: PackageData) => {
setSelectedPackage(pkg)
setIsDetailSheetOpen(true)
}, [])
// 启动依赖包分析 mutation
const analyzeMutation = trpc.devArch!.startAnalyzePackages.useMutation({
onSuccess: (data) => {
// 打开进度对话框
setAnalyzeJobId(String(data.jobId))
setIsAnalyzeDialogOpen(true)
},
onError: (error) => {
toast.error(error.message || '启动依赖包分析失败')
},
})
// 启动分析
const handleStartAnalyze = () => {
analyzeMutation.mutate()
}
// 当包类型加载完成后,设置默认激活标签
useEffect(() => {
if (pkgTypes && pkgTypes.length > 0 && !activeTab) {
setActiveTab(pkgTypes[0].id);
}
}, [pkgTypes, activeTab]);
const isLoading = isLoadingTypes || isLoadingPackages;
// 按优先级搜索过滤name > description > projectRoleSummary > primaryUsagePattern
const getFilteredPackages = useCallback((typeId: string) => {
const packages = packagesByType?.[typeId] || [];
if (!searchQuery) return packages;
const query = searchQuery.toLowerCase();
// 计算每个包的匹配优先级
const packagesWithPriority = packages.map((pkg) => {
let priority = 0;
if (pkg.name.toLowerCase().includes(query)) {
priority = 4;
}
else if (pkg.description.toLowerCase().includes(query)) {
priority = 3;
}
else if (pkg.projectRoleSummary.toLowerCase().includes(query)) {
priority = 2;
}
else if (pkg.primaryUsagePattern.toLowerCase().includes(query)) {
priority = 1;
}
return { pkg, priority };
});
// 过滤出有匹配的包,并按优先级排序
return packagesWithPriority
.filter(({ priority }) => priority > 0)
.sort((a, b) => b.priority - a.priority)
.map(({ pkg }) => pkg);
}, [packagesByType, searchQuery]);
// 将包类型转换为标签项
const tabItems: ResponsiveTabItem[] = pkgTypes?.map((type) => ({
id: type.id,
name: type.name,
description: type.description,
count: packagesByType?.[type.id]?.length || 0,
})) || [];
// 仅当pkgTypes完成加载且为空时才显示"暂无依赖包数据"
if (!isLoading && (!pkgTypes || pkgTypes.length === 0)) {
return (
<div className="text-center py-12 text-muted-foreground">
<Package className="size-10 mx-auto mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
);
}
return (
<>
{isLoading ? (
<ResponsiveTabs.Skeleton className="pb-6">
<Skeleton className="h-10 w-full" />
<div className="grid gap-4 md:grid-cols-1 xl:grid-cols-2 2xl:grid-cols-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-48 w-full rounded-lg" />
))}
</div>
</ResponsiveTabs.Skeleton>
) : (
<ResponsiveTabs
items={tabItems}
value={activeTab}
onValueChange={setActiveTab}
className="pb-6"
showIdBadge
showCountBadge
>
{tabItems.map((item) => {
const filteredPackages = getFilteredPackages(item.id);
return (
<ResponsiveTabs.Content key={item.id} value={item.id}>
<div className="space-y-4">
{/* 搜索栏和操作按钮 */}
<div className="flex items-center gap-3">
<SearchInput
value={searchQuery}
onChange={setSearchQuery}
placeholder="搜索依赖包..."
className="w-80"
/>
<div className="flex-1" />
<PackageAnalyzeTrigger
onStartAnalyze={handleStartAnalyze}
isStarting={analyzeMutation.isPending}
/>
</div>
{/* 包列表 */}
{filteredPackages.length > 0 ? (
<div className="grid gap-4 md:grid-cols-1 xl:grid-cols-2 2xl:grid-cols-4">
{filteredPackages.map((pkg) => (
<PackageCard
key={pkg.name}
pkg={pkg}
onClick={() => handleCardClick(pkg)}
/>
))}
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
<Package className="size-10 mx-auto mb-2 opacity-20" />
<p className="text-sm">
{searchQuery ? '未找到匹配的依赖包' : '暂无依赖包数据'}
</p>
</div>
)}
</div>
</ResponsiveTabs.Content>
);
})}
</ResponsiveTabs>
)}
{/* 依赖包分析进度对话框 */}
<PackageAnalyzeDialog
open={isAnalyzeDialogOpen}
onOpenChange={setIsAnalyzeDialogOpen}
jobId={analyzeJobId}
onAnalyzeCompleted={handleRefreshPackages}
/>
{/* 依赖包详情Sheet */}
<PackageDetailSheet
pkg={selectedPackage}
open={isDetailSheetOpen}
onOpenChange={setIsDetailSheetOpen}
/>
</>
);
}