generated from kevisual/vite-react-template
feat: 添加仓库管理页面和 AI 功能,优化路由和导航
- 新增仓库列表页面,支持查看和管理 CNB 仓库 - 添加 AI 代理系统和状态管理 - 新增 tags-input、popover、textarea、tooltip 等 UI 组件 - 更新依赖:@kevisual/cnb 升级至 0.0.22,添加 idb-keyval - 改进路由守卫:未配置 API Key 时自动跳转配置页 - 优化 Dialog 遮罩层样式和整体布局 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
217
src/app/repo/components/RepoCard.tsx
Normal file
217
src/app/repo/components/RepoCard.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
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 } from 'lucide-react'
|
||||
import { useRepoStore } from '../store'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { myOrgs } from '../store/build'
|
||||
|
||||
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 } = useRepoStore();
|
||||
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)
|
||||
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">
|
||||
<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">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onStartWorkspace(repo)}
|
||||
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"
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p>启动工作区</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={() => 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>
|
||||
|
||||
{repo.topics && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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>
|
||||
))}
|
||||
</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-[2.5rem]">
|
||||
{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