Files
cnb-center/src/pages/repos/components/RepoCard.tsx

353 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
</>
)
}