generated from kevisual/vite-react-template
353 lines
15 KiB
TypeScript
353 lines
15 KiB
TypeScript
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, LinkIcon, ExternalLink, ArrowLeft } from 'lucide-react'
|
||
import { useRepoStore } from '../store'
|
||
import { useMemo, useState } from 'react'
|
||
import { useShallow } from 'zustand/shallow'
|
||
import { myOrgs } from '../store/build'
|
||
import { app } from '@/agents/app'
|
||
import { toast } from 'sonner'
|
||
import { useNavigate } from '@tanstack/react-router'
|
||
import clsx from 'clsx'
|
||
|
||
interface RepoCardProps {
|
||
repo: any
|
||
showReturn?: boolean
|
||
}
|
||
|
||
export function RepoCard({ showReturn = false, repo }: RepoCardProps) {
|
||
const [deletePopoverOpen, setDeletePopoverOpen] = useState(false)
|
||
const store = useRepoStore(useShallow((state) => ({
|
||
workspaceList: state.workspaceList,
|
||
getWorkspaceDetail: state.getWorkspaceDetail,
|
||
getList: state.getList,
|
||
buildUpdate: state.buildUpdate,
|
||
stopWorkspace: state.stopWorkspace,
|
||
setSelectedSyncRepo: state.setSelectedSyncRepo,
|
||
setSyncDialogOpen: state.setSyncDialogOpen,
|
||
startWorkspace: state.startWorkspace,
|
||
setEditRepo: state.setEditRepo,
|
||
setShowEditDialog: state.setShowEditDialog,
|
||
deleteItem: state.deleteItem,
|
||
})));
|
||
const workspace = useMemo(() => {
|
||
return store.workspaceList.find(ws => ws.slug === repo.path)
|
||
}, [store.workspaceList, repo.path])
|
||
const isWorkspaceActive = !!workspace
|
||
const navigate = useNavigate();
|
||
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("知识库创建中")
|
||
store.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 store.buildUpdate({ path: repo.path });
|
||
}
|
||
const handleIssue = (repo: any) => {
|
||
window.open(`https://cnb.cool/${repo.path}/-/issues`)
|
||
}
|
||
const handleSettings = (repo: any) => {
|
||
window.open(`https://cnb.cool/${repo.path}/-/settings`)
|
||
}
|
||
const openInCNB = (isDetail = true) => {
|
||
if (!showReturn && isDetail) {
|
||
navigate({ to: `/repo?repo=${repo.path}` })
|
||
} else {
|
||
window.open(`https://cnb.cool/${repo.path}`, '_blank')
|
||
}
|
||
}
|
||
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-12 md:pb-14">
|
||
<div className="p-4 md:p-6 space-y-3 md:space-y-4">
|
||
<div className="flex items-start justify-between gap-2">
|
||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||
{showReturn && (
|
||
<button
|
||
onClick={() => navigate({ to: '/' })}
|
||
className="cursor-pointer flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 transition-colors shrink-0"
|
||
>
|
||
<ArrowLeft className="w-4 h-4 text-neutral-600" />
|
||
</button>
|
||
)}
|
||
<div
|
||
className="text-base md:text-lg font-bold text-neutral-900 hover:text-neutral-600 transition-colors line-clamp-1 group-hover:underline cursor-pointer"
|
||
onClick={() => {
|
||
openInCNB()
|
||
}}
|
||
>
|
||
{repo.path}
|
||
</div>
|
||
{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>
|
||
)}
|
||
{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-1 md:gap-2 shrink-0">
|
||
{isWorkspaceActive && (
|
||
<TooltipProvider>
|
||
<Tooltip>
|
||
<TooltipTrigger
|
||
render={
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => {
|
||
store.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) {
|
||
store.startWorkspace(repo)
|
||
} else {
|
||
store.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={() => {
|
||
window.open(repo.web_url, '_blank')
|
||
}} className="cursor-pointer">
|
||
<ExternalLink className="w-4 h-4 mr-2" />
|
||
访问仓库
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => {
|
||
store.setEditRepo(repo)
|
||
store.setShowEditDialog(true)
|
||
}} 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={() => handleIssue(repo)} className="cursor-pointer">
|
||
<IssueIcon className="w-4 h-4 mr-2" />
|
||
访问问题
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => handleSettings(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={() => {
|
||
if (repo.path)
|
||
store.deleteItem(repo.path)
|
||
setDeletePopoverOpen(false)
|
||
}}
|
||
>
|
||
确认删除
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-1.5 md: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>
|
||
<div className={clsx(!showReturn && "cursor-pointer")} onClick={() => {
|
||
{ !showReturn && openInCNB(false) }
|
||
}}>
|
||
{repo.site && (
|
||
<div
|
||
className="text-xs text-neutral-500 hover:text-neutral-900 hover:underline flex transition-colors"
|
||
onClick={(e) => {
|
||
window.open(repo.site, '_blank')
|
||
e.stopPropagation()
|
||
}}
|
||
>
|
||
<LinkIcon className="w-4 h-4 shrink-0 mr-2" />
|
||
<div className='truncate grow'>
|
||
{repo.site}
|
||
</div>
|
||
</div>
|
||
)}
|
||
{repo.description && (
|
||
<p className="text-sm text-neutral-600 line-clamp-2 min-h-10">
|
||
{repo.description}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
|
||
<div className="absolute bottom-0 left-0 right-0 flex items-center gap-2 md:gap-4 text-xs text-neutral-500 px-4 md:px-6 py-2 md:py-3 border-t border-neutral-100 bg-neutral-50 overflow-x-auto">
|
||
<span className="flex items-center gap-1 hover:text-neutral-900 transition-colors whitespace-nowrap">
|
||
<Star className="w-3.5 h-3.5" />
|
||
<span className="font-medium">{repo.star_count}</span>
|
||
</span>
|
||
<span className="flex items-center gap-1 hover:text-neutral-900 transition-colors whitespace-nowrap">
|
||
<GitFork className="w-3.5 h-3.5" />
|
||
<span className="font-medium">{repo.fork_count}</span>
|
||
</span>
|
||
<span className="flex items-center gap-1 hover:text-neutral-900 transition-colors whitespace-nowrap">
|
||
<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 hover:text-neutral-900 transition-colors cursor-pointer whitespace-nowrap"
|
||
onClick={() => {
|
||
store.getWorkspaceDetail(workspace)
|
||
}}>
|
||
<Play className="w-3.5 h-3.5" />
|
||
<span className="font-medium">运行中</span>
|
||
</span>}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</>
|
||
)
|
||
}
|