generated from kevisual/vite-react-template
update
This commit is contained in:
321
src/pages/repos/components/RepoCard.tsx
Normal file
321
src/pages/repos/components/RepoCard.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square } from 'lucide-react'
|
||||
import { useRepoStore } from '../store'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useShallow } from 'zustand/shallow'
|
||||
import { myOrgs } from '../store/build'
|
||||
import { app, cnb } from '@/agents/app'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface RepoCardProps {
|
||||
repo: any
|
||||
onStartWorkspace: (repo: any) => void
|
||||
onEdit: (repo: any) => void
|
||||
onIssue: (repo: any) => void
|
||||
onSettings: (repo: any) => void
|
||||
onDelete: (repo: any) => void
|
||||
onSync?: (repo: any) => void
|
||||
}
|
||||
|
||||
export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings, onDelete, onSync }: RepoCardProps) {
|
||||
const [deletePopoverOpen, setDeletePopoverOpen] = useState(false)
|
||||
const { workspaceList, getWorkspaceDetail, getList, buildUpdate, stopWorkspace } = useRepoStore(useShallow((state) => ({
|
||||
workspaceList: state.workspaceList,
|
||||
getWorkspaceDetail: state.getWorkspaceDetail,
|
||||
getList: state.getList,
|
||||
buildUpdate: state.buildUpdate,
|
||||
stopWorkspace: state.stopWorkspace,
|
||||
})));
|
||||
const workspace = useMemo(() => {
|
||||
return workspaceList.find(ws => ws.slug === repo.path)
|
||||
}, [workspaceList, repo.path])
|
||||
const isWorkspaceActive = !!workspace
|
||||
const owner = repo.path.split('/')[0]
|
||||
const isMine = myOrgs.includes(owner)
|
||||
|
||||
const isKnowledge = repo?.flags === "KnowledgeBase"
|
||||
const createKnow = async () => {
|
||||
const res = await app.run({ path: 'cnb', key: 'build-knowledge-base', payload: { repo: repo.path } })
|
||||
if (res.code === 200) {
|
||||
toast.success("知识库创建中")
|
||||
getList(true)
|
||||
}
|
||||
}
|
||||
const onClone = async () => {
|
||||
const url = `git clone https://cnb.cool/${repo.path}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
toast.success('克隆地址已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
toast.error('复制失败')
|
||||
})
|
||||
}
|
||||
const onUpdate = async () => {
|
||||
await buildUpdate({ path: repo.path });
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Card className="relative p-0 overflow-hidden border border-neutral-200 bg-white hover:shadow-xl hover:border-neutral-300 transition-all duration-300 group pb-14">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{isKnowledge && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div className="shrink-0">
|
||||
<BookOpen className="w-5 h-5 text-neutral-700" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>知识库</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<a
|
||||
href={repo.web_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-lg font-bold text-neutral-900 hover:text-neutral-600 transition-colors line-clamp-1 group-hover:underline"
|
||||
>
|
||||
{repo.path}
|
||||
</a>
|
||||
{isWorkspaceActive && (
|
||||
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{isWorkspaceActive && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
stopWorkspace(workspace)
|
||||
}}
|
||||
className="h-8 w-8 p-0 border-neutral-200 hover:border-red-600 hover:bg-red-600 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>停止工作区</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (!isWorkspaceActive) {
|
||||
onStartWorkspace(repo)
|
||||
} else {
|
||||
getWorkspaceDetail(workspace)
|
||||
}
|
||||
|
||||
}}
|
||||
className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
{isWorkspaceActive ? <Eye className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>{isWorkspaceActive ? '查看工作区' : '启动工作区'}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem onClick={() => onEdit(repo)} className="cursor-pointer">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
createKnow()
|
||||
}} className="cursor-pointer">
|
||||
<BookOpen className="w-4 h-4 mr-2" />
|
||||
知识库创建
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
onUpdate()
|
||||
}} className="cursor-pointer">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className={'flex gap-1 items-center'}>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
更新时间
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p>给仓库添加一个空白 commit 的提交,保证在仓库列表中顺序活跃在前面</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onClone} className="cursor-pointer">
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Clone URL
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onIssue(repo)} className="cursor-pointer">
|
||||
<IssueIcon className="w-4 h-4 mr-2" />
|
||||
Issue
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onSettings(repo)} className="cursor-pointer">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
设置
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setDeletePopoverOpen(true)
|
||||
}}
|
||||
className="cursor-pointer text-red-600 focus:text-red-600 focus:bg-red-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Popover open={deletePopoverOpen} onOpenChange={setDeletePopoverOpen}>
|
||||
<PopoverTrigger >
|
||||
<div style={{ display: 'none' }} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">确认删除</h4>
|
||||
<p className="text-sm text-neutral-500">
|
||||
确定要删除仓库 <span className="font-semibold text-neutral-900">{repo.path}</span> 吗?此操作无法撤销。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setDeletePopoverOpen(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-red-600 text-white border-red-600 hover:bg-red-700 hover:border-red-700"
|
||||
onClick={() => {
|
||||
onDelete(repo)
|
||||
setDeletePopoverOpen(false)
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{repo.topics && (<>
|
||||
{
|
||||
repo.topics.split(',').map((topic: string, idx: number) => (
|
||||
<Badge key={idx} variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">
|
||||
{topic.trim()}
|
||||
</Badge>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">{repo.visibility_level}</Badge>
|
||||
</div>
|
||||
|
||||
{repo.site && (
|
||||
<a
|
||||
href={repo.site}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-neutral-500 hover:text-neutral-900 hover:underline block truncate transition-colors"
|
||||
>
|
||||
🔗 {repo.site}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{repo.description && (
|
||||
<p className="text-sm text-neutral-600 line-clamp-2 min-h-10">
|
||||
{repo.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 flex items-center gap-4 text-xs text-neutral-500 px-6 py-3 border-t border-neutral-100 bg-neutral-50">
|
||||
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
|
||||
<Star className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">{repo.star_count}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
|
||||
<GitFork className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">{repo.fork_count}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">{repo.open_issue_count}</span>
|
||||
</span>
|
||||
{isWorkspaceActive && <span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors cursor-pointer"
|
||||
onClick={() => {
|
||||
getWorkspaceDetail(workspace)
|
||||
}}>
|
||||
<Play className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">运行中</span>
|
||||
</span>}
|
||||
{isMine && (
|
||||
<span
|
||||
className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors cursor-pointer"
|
||||
onClick={() => onSync?.(repo)}
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">同步</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user