forked from admin/hair-keeper
Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑
This commit is contained in:
297
src/app/(main)/dev/arch/package/page.dev.tsx
Normal file
297
src/app/(main)/dev/arch/package/page.dev.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user