feat: 添加制品详情页面及相关功能;优化制品列表和数据获取逻辑

This commit is contained in:
xiongxiao
2026-03-27 00:57:47 +08:00
committed by cnb
parent 2f426df210
commit 8984913fcd
7 changed files with 666 additions and 22 deletions

View File

@@ -0,0 +1,235 @@
import { useSearch } from "@tanstack/react-router";
import { MarkItem, usePackageStore } from "../store";
import { PackageItem } from "../store/package-type";
import { useShallow } from "zustand/shallow";
import { useEffect, useState } from "react";
import dayjs from "dayjs";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
CardAction,
} from "@/components/ui/card";
import { SidebarLayout } from "@/pages/sidebar/components";
import { ArrowLeftIcon, Copy, MoreVertical, Info, ExternalLink } from "lucide-react";
import { toast } from "sonner";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export const App = () => {
const searchParams = useSearch({ strict: false }) as { slug?: string; id?: string };
const { slug, id } = searchParams;
const packageStore = usePackageStore(useShallow((state) => ({
packagesList: state.packagesList,
packagesListLoading: state.packagesListLoading,
getPackagesList: state.getPackagesList,
getItem: state.getItem,
})));
const [detailItem, setDetailItem] = useState<MarkItem | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => {
if (slug) {
packageStore.getPackagesList({ slug });
}
}, [slug]);
useEffect(() => {
if (id) {
setDetailLoading(true);
packageStore.getItem(id).then((data) => {
setDetailItem(data);
setDetailLoading(false);
});
}
}, [id]);
const onCopy = (data: PackageItem) => {
// kevisual/dev-env 取 kevisual删除最后一个斜杠和后面的内容
const _group = slug?.split?.('/')?.filter(Boolean);
_group?.shift?.();
const group = _group?.[0];
const value = `docker.cnb.cool/${slug}/${data.package}:latest`
}
const onDetail = (item: PackageItem) => {
}
const onToPage = (item: PackageItem) => {
const type = 'docker';
const url = `https://cnb.cool/${slug}/-/packages?type=${type}&ordering=last_push_at`;
}
return (
<SidebarLayout>
<div className="p-5">
<div className="flex items-center gap-3 mb-5">
<Button variant="ghost" size="sm" onClick={() => history.back()}>
<ArrowLeftIcon className="size-4 mr-1" />
</Button>
<h1 className="text-2xl font-semibold"></h1>
</div>
{id && (
<div className="mb-5">
{detailLoading ? (
<div className="text-center py-4 text-muted-foreground">...</div>
) : detailItem ? (
<Card>
<CardHeader>
<CardTitle>{detailItem.title || '未命名'}</CardTitle>
<CardDescription>{detailItem.summary || '暂无描述'}</CardDescription>
</CardHeader>
<CardContent>
{detailItem.tags && detailItem.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
{detailItem.tags.map((tag, index) => (
<Badge key={index} variant="outline">{tag}</Badge>
))}
</div>
)}
<p className="text-xs text-muted-foreground">
ID: {detailItem.id}
</p>
</CardContent>
</Card>
) : (
<div className="text-center py-4 text-muted-foreground"></div>
)}
</div>
)}
<h2 className="text-xl font-semibold mb-4"></h2>
{packageStore.packagesListLoading ? (
<div className="text-center py-10 text-muted-foreground">...</div>
) : packageStore.packagesList.length === 0 ? (
<div className="text-center py-10 text-muted-foreground"></div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{packageStore.packagesList.map((item: PackageItem) => {
const dockerValue = `docker.cnb.cool/${slug}/${item.package}:latest`;
const dockerPullValue = `docker pull ${dockerValue}`;
return (
<Card key={item.package}>
<CardHeader>
<CardTitle>{item.package || '未命名'}</CardTitle>
<CardDescription className="text-xs">: {item.package_type}</CardDescription>
<CardAction>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(dockerValue).then(() => {
toast.success('已复制 docker value');
});
}}
>
<Copy className="w-4 h-4" />
</Button>
}
/>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 cursor-pointer"
onClick={() => {
toast.info('功能开发中');
}}
>
<Info className="w-4 h-4" />
</Button>
}
/>
<TooltipContent></TooltipContent>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 cursor-pointer"
>
<MoreVertical className="w-4 h-4" />
</Button>
}
/>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(dockerPullValue).then(() => {
toast.success('已复制 docker pull 命令');
});
}}
className="cursor-pointer"
>
<Copy className="w-4 h-4 mr-2" />
docker registry
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const type = 'docker';
const url = `https://cnb.cool/${slug}/-/packages?type=${type}&ordering=last_push_at`;
window.open(url, '_blank');
}}
className="cursor-pointer"
>
<ExternalLink className="w-4 h-4 mr-2" />
page
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardAction>
</CardHeader>
<CardContent className="space-y-2">
{item.labels && item.labels.length > 0 && (
<div className="flex flex-wrap gap-1">
{item.labels.map((label: string, index: number) => (
<Badge key={index} variant="outline">{label}</Badge>
))}
</div>
)}
{item.description && (
<p className="text-sm text-muted-foreground">{item.description}</p>
)}
<p className="text-xs text-muted-foreground">
: {item.pull_count}
</p>
<p className="text-xs text-muted-foreground">
: {item.last_pusher?.nickname || '-'}
</p>
<p className="text-xs text-muted-foreground">
: {item.last_pusher?.push_at ? dayjs(item.last_pusher.push_at).format('YYYY-MM-DD HH:mm:ss') : '-'}
</p>
</CardContent>
</Card>
);
})}
</div>
)}
</div>
</SidebarLayout>
);
};
export default App;