generated from kevisual/vite-react-template
feat: 添加制品详情页面及相关功能;优化制品列表和数据获取逻辑
This commit is contained in:
299
src/modules/package-api.ts
Normal file
299
src/modules/package-api.ts
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import { createQueryApi } from '@kevisual/query/api';
|
||||||
|
const api = {
|
||||||
|
"cnb": {
|
||||||
|
/**
|
||||||
|
* 获取指定制品的详细信息
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.slug - {string} 资源路径, 如 my-org/my-registry
|
||||||
|
* @param data.type - {string} 制品类型
|
||||||
|
* @param data.name - {string} 制品名称
|
||||||
|
*/
|
||||||
|
"get-package": {
|
||||||
|
"path": "cnb",
|
||||||
|
"key": "get-package",
|
||||||
|
"description": "获取制品详情, 参数 slug, type, name",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"package"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"slug": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "资源路径, 如 my-org/my-registry"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品类型"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品名称"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "get-package",
|
||||||
|
"title": "获取制品详情",
|
||||||
|
"summary": "获取指定制品的详细信息",
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取制品标签的详细信息
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.slug - {string} 资源路径, 如 my-org/my-registry
|
||||||
|
* @param data.type - {string} 制品类型
|
||||||
|
* @param data.name - {string} 制品名称
|
||||||
|
* @param data.tag - {string} 标签名称
|
||||||
|
*/
|
||||||
|
"get-package-tag": {
|
||||||
|
"path": "cnb",
|
||||||
|
"key": "get-package-tag",
|
||||||
|
"description": "获取制品标签详情, 参数 slug, type, name, tag",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"package"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"slug": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "资源路径, 如 my-org/my-registry"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品类型"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品名称"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "标签名称"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "get-package-tag",
|
||||||
|
"title": "获取制品标签详情",
|
||||||
|
"summary": "获取制品标签的详细信息",
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 查询制品列表
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.slug - {string} 资源路径, 如 my-org/my-registry
|
||||||
|
* @param data.type - {string} 制品类型: all, docker, helm, docker-model, maven, npm, ohpm, pypi, nuget, composer, conan, cargo
|
||||||
|
* @param data.ordering - {string} 排序类型: pull_count, last_push_at, name_ascend, name_descend
|
||||||
|
* @param data.name - {string} 关键字,搜索制品名称
|
||||||
|
* @param data.page - {number} 页码
|
||||||
|
* @param data.page_size - {number} 每页数量
|
||||||
|
*/
|
||||||
|
"list-packages": {
|
||||||
|
"path": "cnb",
|
||||||
|
"key": "list-packages",
|
||||||
|
"description": "查询制品列表, 参数 slug, type",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"package"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"slug": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "资源路径, 如 my-org/my-registry"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品类型: all, docker, helm, docker-model, maven, npm, ohpm, pypi, nuget, composer, conan, cargo"
|
||||||
|
},
|
||||||
|
"ordering": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "排序类型: pull_count, last_push_at, name_ascend, name_descend",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "关键字,搜索制品名称",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "number",
|
||||||
|
"description": "页码",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "number",
|
||||||
|
"description": "每页数量",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "list-packages",
|
||||||
|
"title": "查询制品列表",
|
||||||
|
"summary": "查询制品列表",
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取制品的标签列表
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.slug - {string} 资源路径, 如 my-org/my-registry
|
||||||
|
* @param data.type - {string} 制品类型
|
||||||
|
* @param data.name - {string} 制品名称
|
||||||
|
* @param data.page - {number} 页码
|
||||||
|
* @param data.page_size - {number} 每页数量
|
||||||
|
*/
|
||||||
|
"list-package-tags": {
|
||||||
|
"path": "cnb",
|
||||||
|
"key": "list-package-tags",
|
||||||
|
"description": "获取制品标签列表, 参数 slug, type, name",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"package"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"slug": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "资源路径, 如 my-org/my-registry"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品类型"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品名称"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "number",
|
||||||
|
"description": "页码",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "number",
|
||||||
|
"description": "每页数量",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "list-package-tags",
|
||||||
|
"title": "获取制品标签列表",
|
||||||
|
"summary": "获取制品的标签列表",
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 删除指定的制品
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.slug - {string} 资源路径, 如 my-org/my-registry
|
||||||
|
* @param data.type - {string} 制品类型
|
||||||
|
* @param data.name - {string} 制品名称
|
||||||
|
*/
|
||||||
|
"delete-package": {
|
||||||
|
"path": "cnb",
|
||||||
|
"key": "delete-package",
|
||||||
|
"description": "删除制品, 参数 slug, type, name",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"package"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"slug": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "资源路径, 如 my-org/my-registry"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品类型"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品名称"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "delete-package",
|
||||||
|
"title": "删除制品",
|
||||||
|
"summary": "删除指定的制品",
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 删除制品的指定标签
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.slug - {string} 资源路径, 如 my-org/my-registry
|
||||||
|
* @param data.type - {string} 制品类型
|
||||||
|
* @param data.name - {string} 制品名称
|
||||||
|
* @param data.tag - {string} 标签名称
|
||||||
|
*/
|
||||||
|
"delete-package-tag": {
|
||||||
|
"path": "cnb",
|
||||||
|
"key": "delete-package-tag",
|
||||||
|
"description": "删除制品标签, 参数 slug, type, name, tag",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"package"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"slug": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "资源路径, 如 my-org/my-registry"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品类型"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "制品名称"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "标签名称"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "delete-package-tag",
|
||||||
|
"title": "删除制品标签",
|
||||||
|
"summary": "删除制品的指定标签",
|
||||||
|
"url": "/api/router",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
const queryApi = createQueryApi({ api });
|
||||||
|
|
||||||
|
export { queryApi };
|
||||||
|
|
||||||
|
// 使用例子,方法为对应的方法,data 为对应的 args 的参数的定义数据
|
||||||
|
// queryApi['user-app'].delete(data)
|
||||||
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;
|
||||||
@@ -13,10 +13,13 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { CreateDialog, EditDialog } from "./components";
|
import { CreateDialog, EditDialog } from "./components";
|
||||||
import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon } from "lucide-react";
|
import { SearchIcon, RefreshCwIcon, PlusIcon, PencilIcon, TrashIcon, EyeIcon } from "lucide-react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { SidebarLayout } from "@/pages/sidebar/components";
|
import { SidebarLayout } from "@/pages/sidebar/components";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const packageStore = usePackageStore(useShallow((state: PackageState) => {
|
const packageStore = usePackageStore(useShallow((state: PackageState) => {
|
||||||
return {
|
return {
|
||||||
list: state.list,
|
list: state.list,
|
||||||
@@ -31,6 +34,7 @@ export const App = () => {
|
|||||||
setShowEditDialog: state.setShowEditDialog,
|
setShowEditDialog: state.setShowEditDialog,
|
||||||
editingItem: state.editingItem,
|
editingItem: state.editingItem,
|
||||||
setEditingItem: state.setEditingItem,
|
setEditingItem: state.setEditingItem,
|
||||||
|
getItem: state.getItem,
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -49,8 +53,13 @@ export const App = () => {
|
|||||||
packageStore.getList({ search });
|
packageStore.getList({ search });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item: any) => {
|
const handleEdit = async (item: any) => {
|
||||||
packageStore.setEditingItem(item);
|
const data = await packageStore.getItem(item.id);
|
||||||
|
if (!data) {
|
||||||
|
toast.error('未找到制品数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packageStore.setEditingItem(data);
|
||||||
packageStore.setShowEditDialog(true);
|
packageStore.setShowEditDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,9 +116,6 @@ export const App = () => {
|
|||||||
{item.summary && (
|
{item.summary && (
|
||||||
<p className="text-sm text-muted-foreground">{item.summary}</p>
|
<p className="text-sm text-muted-foreground">{item.summary}</p>
|
||||||
)}
|
)}
|
||||||
{item.description && (
|
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">{item.description}</p>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
创建时间: {item.createdAt ? dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
</p>
|
</p>
|
||||||
@@ -117,6 +123,9 @@ export const App = () => {
|
|||||||
更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
更新时间: {item.updatedAt ? dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => navigate({ to: '/cnb-packages/detail', search: { slug: item.title, id: item.id } })}>
|
||||||
|
<EyeIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={() => handleEdit(item)}>
|
<Button variant="outline" size="sm" onClick={() => handleEdit(item)}>
|
||||||
<PencilIcon className="size-3.5" />
|
<PencilIcon className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,37 +1,43 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { queryApi } from '@/modules/mark-api';
|
import { queryApi as markApi } from '@/modules/mark-api';
|
||||||
|
import { queryApi as cnbApi } from '@/modules/package-api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { PackageItem } from './package-type';
|
||||||
|
|
||||||
type PackageItem = {
|
export type MarkItem = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
tags?: string[];
|
tags: string[];
|
||||||
link?: string;
|
link: string;
|
||||||
summary?: string;
|
summary: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
data?: any;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PackageState = {
|
type PackageState = {
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
setEdit: (edit: boolean) => void;
|
setEdit: (edit: boolean) => void;
|
||||||
list: PackageItem[];
|
list: MarkItem[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
setLoading: (loading: boolean) => void;
|
setLoading: (loading: boolean) => void;
|
||||||
|
// CNB packages list
|
||||||
|
packagesList: PackageItem[];
|
||||||
|
packagesListLoading: boolean;
|
||||||
|
getPackagesList: (params: { slug: string, type?: string, ordering?: string, name?: string, page?: number, pageSize?: number }) => Promise<PackageItem[]>;
|
||||||
// Dialog states
|
// Dialog states
|
||||||
showCreateDialog: boolean;
|
showCreateDialog: boolean;
|
||||||
setShowCreateDialog: (show: boolean) => void;
|
setShowCreateDialog: (show: boolean) => void;
|
||||||
showEditDialog: boolean;
|
showEditDialog: boolean;
|
||||||
setShowEditDialog: (show: boolean) => void;
|
setShowEditDialog: (show: boolean) => void;
|
||||||
editingItem: PackageItem | null;
|
editingItem: MarkItem | null;
|
||||||
setEditingItem: (item: PackageItem | null) => void;
|
setEditingItem: (item: MarkItem | null) => void;
|
||||||
// Data operations
|
// Data operations MarkItem
|
||||||
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
|
getList: (params?: { search?: string, page?: number, pageSize?: number }) => Promise<void>;
|
||||||
createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
createItem: (data: { title: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||||
updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
updateItem: (id: string, data: { title?: string, tags?: string[], link?: string, summary?: string, description?: string }) => Promise<void>;
|
||||||
deleteItem: (id: string) => Promise<void>;
|
deleteItem: (id: string) => Promise<void>;
|
||||||
getItem: (id: string) => Promise<PackageItem | null>;
|
getItem: (id: string) => Promise<MarkItem | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { PackageState, PackageItem };
|
export type { PackageState, PackageItem };
|
||||||
@@ -42,6 +48,35 @@ export const usePackageStore = create<PackageState>((set, get) => ({
|
|||||||
list: [],
|
list: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
setLoading: (loading) => set({ loading }),
|
setLoading: (loading) => set({ loading }),
|
||||||
|
packagesList: [],
|
||||||
|
packagesListLoading: false,
|
||||||
|
getPackagesList: async (params: { slug: string, type?: string, ordering?: string, name?: string, page?: number, pageSize?: number }) => {
|
||||||
|
const { slug, type = 'all', ordering, name, page = 1, pageSize = 20 } = params;
|
||||||
|
set({ packagesListLoading: true });
|
||||||
|
try {
|
||||||
|
const res = await cnbApi.cnb['list-packages']({
|
||||||
|
slug,
|
||||||
|
type,
|
||||||
|
ordering,
|
||||||
|
name,
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
set({ packagesList: res.data?.list || [] });
|
||||||
|
return res.data?.list || [];
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || '获取制品列表失败');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取制品列表失败', e);
|
||||||
|
toast.error('获取制品列表失败');
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
set({ packagesListLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
showCreateDialog: false,
|
showCreateDialog: false,
|
||||||
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
|
setShowCreateDialog: (show) => set({ showCreateDialog: show }),
|
||||||
showEditDialog: false,
|
showEditDialog: false,
|
||||||
@@ -53,7 +88,7 @@ export const usePackageStore = create<PackageState>((set, get) => ({
|
|||||||
const { page = 1, pageSize = 20, search } = params;
|
const { page = 1, pageSize = 20, search } = params;
|
||||||
set({ loading: true });
|
set({ loading: true });
|
||||||
try {
|
try {
|
||||||
const res = await queryApi.mark.list({
|
const res = await markApi.mark.list({
|
||||||
markType: 'cnb-packages',
|
markType: 'cnb-packages',
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -75,7 +110,7 @@ export const usePackageStore = create<PackageState>((set, get) => ({
|
|||||||
|
|
||||||
createItem: async (data) => {
|
createItem: async (data) => {
|
||||||
try {
|
try {
|
||||||
const res = await queryApi.mark.create({
|
const res = await markApi.mark.create({
|
||||||
title: data.title,
|
title: data.title,
|
||||||
markType: 'cnb-packages',
|
markType: 'cnb-packages',
|
||||||
tags: data.tags || [],
|
tags: data.tags || [],
|
||||||
@@ -98,7 +133,7 @@ export const usePackageStore = create<PackageState>((set, get) => ({
|
|||||||
|
|
||||||
updateItem: async (id, data) => {
|
updateItem: async (id, data) => {
|
||||||
try {
|
try {
|
||||||
const res = await queryApi.mark.update({
|
const res = await markApi.mark.update({
|
||||||
data: {
|
data: {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
id,
|
id,
|
||||||
@@ -124,7 +159,7 @@ export const usePackageStore = create<PackageState>((set, get) => ({
|
|||||||
|
|
||||||
deleteItem: async (id) => {
|
deleteItem: async (id) => {
|
||||||
try {
|
try {
|
||||||
const res = await queryApi.mark.delete({ id });
|
const res = await markApi.mark.delete({ id });
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
toast.success('删除成功');
|
toast.success('删除成功');
|
||||||
get().getList();
|
get().getList();
|
||||||
@@ -139,7 +174,7 @@ export const usePackageStore = create<PackageState>((set, get) => ({
|
|||||||
|
|
||||||
getItem: async (id) => {
|
getItem: async (id) => {
|
||||||
try {
|
try {
|
||||||
const res = await queryApi.mark.get({ id });
|
const res = await markApi.mark.get({ id });
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
35
src/pages/cnb-packages/store/package-type.ts
Normal file
35
src/pages/cnb-packages/store/package-type.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
type PackageType = 'docker' | 'npm';
|
||||||
|
export type DockerPackage = {
|
||||||
|
package: string;
|
||||||
|
pull_count: number;
|
||||||
|
description: string;
|
||||||
|
labels: string[];
|
||||||
|
package_type: PackageType;
|
||||||
|
count: number;
|
||||||
|
last_pusher: {
|
||||||
|
name: string;
|
||||||
|
nickname: string;
|
||||||
|
push_at: string;
|
||||||
|
};
|
||||||
|
recent_pull_count: number;
|
||||||
|
last_artifact_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NpmRegistry = {
|
||||||
|
name: string;
|
||||||
|
package: string;
|
||||||
|
pull_count: number;
|
||||||
|
description: string;
|
||||||
|
labels: string[] | null;
|
||||||
|
package_type: PackageType;
|
||||||
|
count: number;
|
||||||
|
last_pusher: {
|
||||||
|
name: string;
|
||||||
|
nickname: string;
|
||||||
|
push_at: string;
|
||||||
|
};
|
||||||
|
recent_pull_count: number;
|
||||||
|
last_artifact_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PackageItem = DockerPackage | NpmRegistry;
|
||||||
@@ -18,6 +18,7 @@ import { Route as ConfigIndexRouteImport } from './routes/config/index'
|
|||||||
import { Route as CnbPackagesIndexRouteImport } from './routes/cnb-packages/index'
|
import { Route as CnbPackagesIndexRouteImport } from './routes/cnb-packages/index'
|
||||||
import { Route as CloudEnvIndexRouteImport } from './routes/cloud-env/index'
|
import { Route as CloudEnvIndexRouteImport } from './routes/cloud-env/index'
|
||||||
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
import { Route as ConfigGiteaRouteImport } from './routes/config/gitea'
|
||||||
|
import { Route as CnbPackagesDetailIndexRouteImport } from './routes/cnb-packages/detail/index'
|
||||||
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
@@ -64,6 +65,11 @@ const ConfigGiteaRoute = ConfigGiteaRouteImport.update({
|
|||||||
path: '/config/gitea',
|
path: '/config/gitea',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const CnbPackagesDetailIndexRoute = CnbPackagesDetailIndexRouteImport.update({
|
||||||
|
id: '/cnb-packages/detail/',
|
||||||
|
path: '/cnb-packages/detail/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -75,6 +81,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/config/': typeof ConfigIndexRoute
|
'/config/': typeof ConfigIndexRoute
|
||||||
'/repo/': typeof RepoIndexRoute
|
'/repo/': typeof RepoIndexRoute
|
||||||
'/workspaces/': typeof WorkspacesIndexRoute
|
'/workspaces/': typeof WorkspacesIndexRoute
|
||||||
|
'/cnb-packages/detail/': typeof CnbPackagesDetailIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -86,6 +93,7 @@ export interface FileRoutesByTo {
|
|||||||
'/config': typeof ConfigIndexRoute
|
'/config': typeof ConfigIndexRoute
|
||||||
'/repo': typeof RepoIndexRoute
|
'/repo': typeof RepoIndexRoute
|
||||||
'/workspaces': typeof WorkspacesIndexRoute
|
'/workspaces': typeof WorkspacesIndexRoute
|
||||||
|
'/cnb-packages/detail': typeof CnbPackagesDetailIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@@ -98,6 +106,7 @@ export interface FileRoutesById {
|
|||||||
'/config/': typeof ConfigIndexRoute
|
'/config/': typeof ConfigIndexRoute
|
||||||
'/repo/': typeof RepoIndexRoute
|
'/repo/': typeof RepoIndexRoute
|
||||||
'/workspaces/': typeof WorkspacesIndexRoute
|
'/workspaces/': typeof WorkspacesIndexRoute
|
||||||
|
'/cnb-packages/detail/': typeof CnbPackagesDetailIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@@ -111,6 +120,7 @@ export interface FileRouteTypes {
|
|||||||
| '/config/'
|
| '/config/'
|
||||||
| '/repo/'
|
| '/repo/'
|
||||||
| '/workspaces/'
|
| '/workspaces/'
|
||||||
|
| '/cnb-packages/detail/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@@ -122,6 +132,7 @@ export interface FileRouteTypes {
|
|||||||
| '/config'
|
| '/config'
|
||||||
| '/repo'
|
| '/repo'
|
||||||
| '/workspaces'
|
| '/workspaces'
|
||||||
|
| '/cnb-packages/detail'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@@ -133,6 +144,7 @@ export interface FileRouteTypes {
|
|||||||
| '/config/'
|
| '/config/'
|
||||||
| '/repo/'
|
| '/repo/'
|
||||||
| '/workspaces/'
|
| '/workspaces/'
|
||||||
|
| '/cnb-packages/detail/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -145,6 +157,7 @@ export interface RootRouteChildren {
|
|||||||
ConfigIndexRoute: typeof ConfigIndexRoute
|
ConfigIndexRoute: typeof ConfigIndexRoute
|
||||||
RepoIndexRoute: typeof RepoIndexRoute
|
RepoIndexRoute: typeof RepoIndexRoute
|
||||||
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
|
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
|
||||||
|
CnbPackagesDetailIndexRoute: typeof CnbPackagesDetailIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -212,6 +225,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ConfigGiteaRouteImport
|
preLoaderRoute: typeof ConfigGiteaRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/cnb-packages/detail/': {
|
||||||
|
id: '/cnb-packages/detail/'
|
||||||
|
path: '/cnb-packages/detail'
|
||||||
|
fullPath: '/cnb-packages/detail/'
|
||||||
|
preLoaderRoute: typeof CnbPackagesDetailIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +245,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
ConfigIndexRoute: ConfigIndexRoute,
|
ConfigIndexRoute: ConfigIndexRoute,
|
||||||
RepoIndexRoute: RepoIndexRoute,
|
RepoIndexRoute: RepoIndexRoute,
|
||||||
WorkspacesIndexRoute: WorkspacesIndexRoute,
|
WorkspacesIndexRoute: WorkspacesIndexRoute,
|
||||||
|
CnbPackagesDetailIndexRoute: CnbPackagesDetailIndexRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
10
src/routes/cnb-packages/detail/index.tsx
Normal file
10
src/routes/cnb-packages/detail/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import App from '@/pages/cnb-packages/detail/page'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/cnb-packages/detail/')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <App />
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user