generated from kevisual/vite-react-template
feat: 添加制品详情页面及相关功能;优化制品列表和数据获取逻辑
This commit is contained in:
235
src/pages/cnb-packages/detail/page.tsx
Normal file
235
src/pages/cnb-packages/detail/page.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user