增加团队管理页面

This commit is contained in:
dengqichen 2025-10-29 18:04:53 +08:00
parent 945d827eb8
commit 3ad79299c3
2 changed files with 147 additions and 106 deletions

View File

@ -622,7 +622,7 @@ const GitManager: React.FC = () => {
className={`h-4 w-4 ${syncing.groups ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.groups ? 'animate-spin' : ''}`}
/> />
</Button> </Button>
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@ -638,13 +638,13 @@ const GitManager: React.FC = () => {
</CardHeader> </CardHeader>
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="p-2 space-y-1"> <div className="p-2 space-y-1 min-h-[calc(100vh-280px)]">
{loading.groups ? ( {loading.groups ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center h-full min-h-[400px]">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : filteredGroupTree.length === 0 ? ( ) : filteredGroupTree.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground"> <div className="flex flex-col items-center justify-center h-full min-h-[400px] text-muted-foreground">
<FolderGit2 className="h-12 w-12 mb-2 opacity-20" /> <FolderGit2 className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p> <p className="text-sm"></p>
</div> </div>
@ -654,7 +654,7 @@ const GitManager: React.FC = () => {
</div> </div>
</ScrollArea> </ScrollArea>
</Card> </Card>
{/* 中栏:项目列表 */} {/* 中栏:项目列表 */}
@ -663,9 +663,9 @@ const GitManager: React.FC = () => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base"> <CardTitle className="text-base">
{selectedGroup && `(${filteredProjects.length})`} {selectedGroup && `(${filteredProjects.length})`}
</CardTitle> </CardTitle>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
@ -675,8 +675,8 @@ const GitManager: React.FC = () => {
<RefreshCw <RefreshCw
className={`h-4 w-4 ${syncing.projects ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.projects ? 'animate-spin' : ''}`}
/> />
</Button> </Button>
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@ -760,7 +760,7 @@ const GitManager: React.FC = () => {
</span> </span>
)} )}
</div> </div>
</div> </div>
{project.webUrl && ( {project.webUrl && (
<Tooltip> <Tooltip>
@ -772,11 +772,11 @@ const GitManager: React.FC = () => {
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex-shrink-0" className="flex-shrink-0"
> >
<Button variant="ghost" size="icon" className="h-6 w-6"> <Button variant="ghost" size="icon" className="h-6 w-6">
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
</Button> </Button>
</a> </a>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> GitLab </p> <p> GitLab </p>
@ -788,8 +788,8 @@ const GitManager: React.FC = () => {
))} ))}
</div> </div>
)} )}
</div> </div>
</Card> </Card>
{/* 右栏:分支列表 */} {/* 右栏:分支列表 */}
<Card className="col-span-4 flex flex-col min-h-0"> <Card className="col-span-4 flex flex-col min-h-0">
@ -798,8 +798,8 @@ const GitManager: React.FC = () => {
<CardTitle className="text-base"> <CardTitle className="text-base">
{selectedProject && `(${filteredBranches.length})`} {selectedProject && `(${filteredBranches.length})`}
</CardTitle> </CardTitle>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={handleSyncBranches} onClick={handleSyncBranches}
@ -808,8 +808,8 @@ const GitManager: React.FC = () => {
<RefreshCw <RefreshCw
className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`}
/> />
</Button> </Button>
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@ -864,7 +864,7 @@ const GitManager: React.FC = () => {
<Badge variant="secondary" className="text-xs flex items-center gap-1 cursor-help"> <Badge variant="secondary" className="text-xs flex items-center gap-1 cursor-help">
<Shield className="h-3 w-3" /> <Shield className="h-3 w-3" />
</Badge> </Badge>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p></p> <p></p>
@ -900,10 +900,10 @@ const GitManager: React.FC = () => {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<a <a
href={branch.webUrl} href={branch.webUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex-shrink-0" className="flex-shrink-0"
> >
<Button variant="ghost" size="icon" className="h-6 w-6"> <Button variant="ghost" size="icon" className="h-6 w-6">
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
</Button> </Button>

View File

@ -3,6 +3,7 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area';
import { import {
Activity, Activity,
CheckCircle2, CheckCircle2,
@ -119,7 +120,11 @@ const Dashboard: React.FC = () => {
<Card className="border-l-4 border-l-orange-500"> <Card className="border-l-4 border-l-orange-500">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle> <CardTitle className="text-sm font-medium"></CardTitle>
<Loader2 className="h-4 w-4 text-muted-foreground animate-spin" /> {data?.summary.running && data.summary.running > 0 ? (
<Loader2 className="h-4 w-4 text-orange-500 animate-spin" />
) : (
<Activity className="h-4 w-4 text-muted-foreground" />
)}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{data?.summary.running || 0}</div> <div className="text-2xl font-bold">{data?.summary.running || 0}</div>
@ -277,114 +282,150 @@ const Dashboard: React.FC = () => {
{/* 状态分组视图 */} {/* 状态分组视图 */}
<TabsContent value="status"> <TabsContent value="status">
<div className="space-y-4"> <div className="grid gap-4 md:grid-cols-4 h-[calc(100vh-280px)]">
{/* 运行中 */} {/* 运行中 */}
<Card> <Card className="border-l-4 border-l-orange-500 flex flex-col">
<CardHeader className="pb-3"> <CardHeader className="pb-3 flex-shrink-0">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-sm flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse" /> <div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse" />
({groupedJobs.running.length})
</CardTitle> </CardTitle>
<div className="text-2xl font-bold">{groupedJobs.running.length}</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex-1 overflow-hidden">
{groupedJobs.running.length === 0 ? ( <ScrollArea className="h-full pr-3">
<p className="text-sm text-muted-foreground"></p> {groupedJobs.running.length === 0 ? (
) : ( <div className="text-center py-8 text-muted-foreground text-sm">
<div className="space-y-2"> <Loader2 className="h-8 w-8 mx-auto mb-2 opacity-20" />
{groupedJobs.running.map((job) => { <p></p>
const runningJob = data?.runningJobs.find(r => r.jobId === job.id); </div>
return ( ) : (
<div key={job.id} className="flex items-center gap-3 p-2 border rounded"> <div className="space-y-3">
<div className="flex-1"> {groupedJobs.running.map((job) => {
<p className="font-medium">{job.jobName}</p> const runningJob = data?.runningJobs.find(r => r.jobId === job.id);
return (
<div key={job.id} className="p-3 border rounded-lg bg-card hover:bg-accent transition-colors">
<p className="font-medium text-sm mb-2 truncate" title={job.jobName}>
{job.jobName}
</p>
{runningJob && ( {runningJob && (
<Progress value={runningJob.progress} className="h-2 mt-2" /> <>
<Progress value={runningJob.progress} className="h-2 mb-2" />
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{runningJob.message || '执行中...'}</span>
<Badge variant="outline" className="text-xs">
{runningJob.progress}%
</Badge>
</div>
</>
)} )}
</div> </div>
{runningJob && ( );
<Badge variant="outline">{runningJob.progress}%</Badge> })}
)} </div>
</div> )}
); </ScrollArea>
})}
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
{/* 已暂停 */} {/* 已暂停 */}
<Card> <Card className="border-l-4 border-l-yellow-500 flex flex-col">
<CardHeader className="pb-3"> <CardHeader className="pb-3 flex-shrink-0">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-sm flex items-center gap-2">
<Pause className="h-4 w-4 text-yellow-600" /> <Pause className="h-4 w-4 text-yellow-600" />
({groupedJobs.paused.length})
</CardTitle> </CardTitle>
<div className="text-2xl font-bold">{groupedJobs.paused.length}</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex-1 overflow-hidden">
{groupedJobs.paused.length === 0 ? ( <ScrollArea className="h-full pr-3">
<p className="text-sm text-muted-foreground"></p> {groupedJobs.paused.length === 0 ? (
) : ( <div className="text-center py-8 text-muted-foreground text-sm">
<div className="space-y-2"> <Pause className="h-8 w-8 mx-auto mb-2 opacity-20" />
{groupedJobs.paused.map((job) => ( <p></p>
<div key={job.id} className="flex items-center justify-between p-2 border rounded"> </div>
<p className="font-medium">{job.jobName}</p> ) : (
<p className="text-sm text-muted-foreground"> <div className="space-y-2">
: {job.lastExecuteTime ? dayjs(job.lastExecuteTime).fromNow() : '-'} {groupedJobs.paused.map((job) => (
</p> <div key={job.id} className="p-3 border rounded-lg bg-card hover:bg-accent transition-colors">
</div> <p className="font-medium text-sm mb-1 truncate" title={job.jobName}>
))} {job.jobName}
</div> </p>
)} <p className="text-xs text-muted-foreground">
: {job.lastExecuteTime ? dayjs(job.lastExecuteTime).fromNow() : '-'}
</p>
</div>
))}
</div>
)}
</ScrollArea>
</CardContent> </CardContent>
</Card> </Card>
{/* 空闲中 */} {/* 空闲中 */}
<Card> <Card className="border-l-4 border-l-green-500 flex flex-col">
<CardHeader className="pb-3"> <CardHeader className="pb-3 flex-shrink-0">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-sm flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600" /> <CheckCircle2 className="h-4 w-4 text-green-600" />
({groupedJobs.idle.length})
</CardTitle> </CardTitle>
<div className="text-2xl font-bold">{groupedJobs.idle.length}</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex-1 overflow-hidden">
{groupedJobs.idle.length === 0 ? ( <ScrollArea className="h-full pr-3">
<p className="text-sm text-muted-foreground"></p> {groupedJobs.idle.length === 0 ? (
) : ( <div className="text-center py-8 text-muted-foreground text-sm">
<div className="grid gap-2 md:grid-cols-2"> <CheckCircle2 className="h-8 w-8 mx-auto mb-2 opacity-20" />
{groupedJobs.idle.map((job) => ( <p></p>
<div key={job.id} className="p-2 border rounded"> </div>
<p className="font-medium truncate">{job.jobName}</p> ) : (
<p className="text-sm text-muted-foreground"> <div className="space-y-2">
: {job.nextExecuteTime ? dayjs(job.nextExecuteTime).format('MM-DD HH:mm') : '-'} {groupedJobs.idle.map((job) => (
</p> <div key={job.id} className="p-3 border rounded-lg bg-card hover:bg-accent transition-colors">
</div> <p className="font-medium text-sm mb-1 truncate" title={job.jobName}>
))} {job.jobName}
</div> </p>
)} <p className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="h-3 w-3" />
: {job.nextExecuteTime ? dayjs(job.nextExecuteTime).format('MM-DD HH:mm') : '-'}
</p>
</div>
))}
</div>
)}
</ScrollArea>
</CardContent> </CardContent>
</Card> </Card>
{/* 已禁用 */} {/* 已禁用 */}
<Card> <Card className="border-l-4 border-l-gray-400 flex flex-col">
<CardHeader className="pb-3"> <CardHeader className="pb-3 flex-shrink-0">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-sm flex items-center gap-2">
<XCircle className="h-4 w-4 text-gray-500" /> <XCircle className="h-4 w-4 text-gray-500" />
({groupedJobs.disabled.length})
</CardTitle> </CardTitle>
<div className="text-2xl font-bold">{groupedJobs.disabled.length}</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex-1 overflow-hidden">
{groupedJobs.disabled.length === 0 ? ( <ScrollArea className="h-full pr-3">
<p className="text-sm text-muted-foreground"></p> {groupedJobs.disabled.length === 0 ? (
) : ( <div className="text-center py-8 text-muted-foreground text-sm">
<div className="grid gap-2 md:grid-cols-2"> <XCircle className="h-8 w-8 mx-auto mb-2 opacity-20" />
{groupedJobs.disabled.map((job) => ( <p></p>
<div key={job.id} className="p-2 border rounded opacity-60"> </div>
<p className="font-medium truncate">{job.jobName}</p> ) : (
<p className="text-sm text-muted-foreground"></p> <div className="space-y-2">
</div> {groupedJobs.disabled.map((job) => (
))} <div key={job.id} className="p-3 border rounded-lg bg-card hover:bg-accent transition-colors opacity-60">
</div> <p className="font-medium text-sm mb-1 truncate" title={job.jobName}>
)} {job.jobName}
</p>
<p className="text-xs text-muted-foreground"></p>
</div>
))}
</div>
)}
</ScrollArea>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>