增加团队管理页面

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

View File

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