增加团队管理页面

This commit is contained in:
dengqichen 2025-10-30 17:07:05 +08:00
parent 1dbb411e08
commit 10bfa7bcbd
2 changed files with 702 additions and 611 deletions

View File

@ -1,21 +1,21 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, {useState, useEffect, useMemo} from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; import {Table, TableHeader, TableBody, TableRow, TableHead, TableCell} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge'; import {Badge} from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import {Button} from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import {Input} from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
import { DataTablePagination } from '@/components/ui/pagination'; import {DataTablePagination} from '@/components/ui/pagination';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
import { import {
Loader2, Plus, Search, Edit, Trash2, Play, Pause, Loader2, Plus, Search, Edit, Trash2, Play, Pause,
Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText, BarChart3, List, Ban
} from 'lucide-react'; } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast'; import {useToast} from '@/components/ui/use-toast';
import { getScheduleJobs, getJobCategoryList, startJob, pauseJob, resumeJob, stopJob, triggerJob, deleteScheduleJob } from './service'; import {getScheduleJobs, getJobCategoryList, startJob, pauseJob, resumeJob, stopJob, triggerJob, disableJob, deleteScheduleJob, enableJob} from './service';
import type { ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus } from './types'; import type {ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus} from './types';
import type { Page } from '@/types/base'; import type {Page} from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; import {DEFAULT_PAGE_SIZE, DEFAULT_CURRENT} from '@/utils/page';
import CategoryManageDialog from './components/CategoryManageDialog'; import CategoryManageDialog from './components/CategoryManageDialog';
import JobLogDialog from './components/JobLogDialog'; import JobLogDialog from './components/JobLogDialog';
import JobEditDialog from './components/JobEditDialog'; import JobEditDialog from './components/JobEditDialog';
@ -36,7 +36,7 @@ import dayjs from 'dayjs';
* *
*/ */
const ScheduleJobList: React.FC = () => { const ScheduleJobList: React.FC = () => {
const { toast } = useToast(); const {toast} = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<ScheduleJobResponse> | null>(null); const [data, setData] = useState<Page<ScheduleJobResponse> | null>(null);
const [categories, setCategories] = useState<JobCategoryResponse[]>([]); const [categories, setCategories] = useState<JobCategoryResponse[]>([]);
@ -88,7 +88,7 @@ const ScheduleJobList: React.FC = () => {
// 搜索 // 搜索
const handleSearch = () => { const handleSearch = () => {
setQuery(prev => ({ ...prev, pageNum: 0 })); setQuery(prev => ({...prev, pageNum: 0}));
}; };
// 重置 // 重置
@ -236,6 +236,44 @@ const ScheduleJobList: React.FC = () => {
} }
}; };
// 禁用任务
const handleDisable = async (record: ScheduleJobResponse) => {
try {
await disableJob(record.id);
toast({
title: '禁用成功',
description: `任务 "${record.jobName}" 已禁用`,
});
loadData();
} catch (error) {
console.error('禁用失败:', error);
toast({
variant: 'destructive',
title: '禁用失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 启用(解除禁用)任务
const handleEnable = async (record: ScheduleJobResponse) => {
try {
await enableJob(record.id);
toast({
title: '启用成功',
description: `任务 "${record.jobName}" 已启用`,
});
loadData();
} catch (error) {
console.error('启用失败:', error);
toast({
variant: 'destructive',
title: '启用失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 查看日志 // 查看日志
const handleViewLog = (record: ScheduleJobResponse) => { const handleViewLog = (record: ScheduleJobResponse) => {
setSelectedJob(record); setSelectedJob(record);
@ -249,15 +287,15 @@ const ScheduleJobList: React.FC = () => {
text: string; text: string;
icon: React.ElementType icon: React.ElementType
}> = { }> = {
ENABLED: { variant: 'default', text: '启用', icon: CheckCircle2 }, ENABLED: {variant: 'default', text: '启用', icon: CheckCircle2},
DISABLED: { variant: 'secondary', text: '禁用', icon: XCircle }, DISABLED: {variant: 'secondary', text: '禁用', icon: XCircle},
PAUSED: { variant: 'outline', text: '暂停', icon: Pause }, PAUSED: {variant: 'outline', text: '暂停', icon: Pause},
}; };
const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock }; const statusInfo = statusMap[status] || {variant: 'outline', text: status, icon: Clock};
const Icon = statusInfo.icon; const Icon = statusInfo.icon;
return ( return (
<Badge variant={statusInfo.variant} className="inline-flex items-center gap-1"> <Badge variant={statusInfo.variant} className="inline-flex items-center gap-1">
<Icon className="h-3 w-3" /> <Icon className="h-3 w-3"/>
{statusInfo.text} {statusInfo.text}
</Badge> </Badge>
); );
@ -268,7 +306,7 @@ const ScheduleJobList: React.FC = () => {
const total = data?.totalElements || 0; const total = data?.totalElements || 0;
const enabledCount = data?.content?.filter(d => d.status === 'ENABLED').length || 0; const enabledCount = data?.content?.filter(d => d.status === 'ENABLED').length || 0;
const pausedCount = data?.content?.filter(d => d.status === 'PAUSED').length || 0; const pausedCount = data?.content?.filter(d => d.status === 'PAUSED').length || 0;
return { total, enabledCount, pausedCount }; return {total, enabledCount, pausedCount};
}, [data]); }, [data]);
return ( return (
@ -284,11 +322,11 @@ const ScheduleJobList: React.FC = () => {
<Tabs defaultValue="list" className="space-y-6"> <Tabs defaultValue="list" className="space-y-6">
<TabsList> <TabsList>
<TabsTrigger value="list" className="gap-2"> <TabsTrigger value="list" className="gap-2">
<List className="h-4 w-4" /> <List className="h-4 w-4"/>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="dashboard" className="gap-2"> <TabsTrigger value="dashboard" className="gap-2">
<BarChart3 className="h-4 w-4" /> <BarChart3 className="h-4 w-4"/>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@ -300,7 +338,7 @@ const ScheduleJobList: React.FC = () => {
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20"> <Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-blue-700"></CardTitle> <CardTitle className="text-sm font-medium text-blue-700"></CardTitle>
<Activity className="h-4 w-4 text-blue-500" /> <Activity className="h-4 w-4 text-blue-500"/>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{stats.total}</div> <div className="text-2xl font-bold">{stats.total}</div>
@ -310,7 +348,7 @@ const ScheduleJobList: React.FC = () => {
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20"> <Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-green-700"></CardTitle> <CardTitle className="text-sm font-medium text-green-700"></CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" /> <CheckCircle2 className="h-4 w-4 text-green-500"/>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{stats.enabledCount}</div> <div className="text-2xl font-bold">{stats.enabledCount}</div>
@ -320,7 +358,7 @@ const ScheduleJobList: React.FC = () => {
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20"> <Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-yellow-700"></CardTitle> <CardTitle className="text-sm font-medium text-yellow-700"></CardTitle>
<Pause className="h-4 w-4 text-yellow-500" /> <Pause className="h-4 w-4 text-yellow-500"/>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{stats.pausedCount}</div> <div className="text-2xl font-bold">{stats.pausedCount}</div>
@ -334,11 +372,11 @@ const ScheduleJobList: React.FC = () => {
<CardTitle></CardTitle> <CardTitle></CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}> <Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<FolderKanban className="h-4 w-4 mr-2" /> <FolderKanban className="h-4 w-4 mr-2"/>
</Button> </Button>
<Button onClick={handleCreate}> <Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2"/>
</Button> </Button>
</div> </div>
@ -350,16 +388,16 @@ const ScheduleJobList: React.FC = () => {
<Input <Input
placeholder="搜索任务名称..." placeholder="搜索任务名称..."
value={query.jobName} value={query.jobName}
onChange={(e) => setQuery({ ...query, jobName: e.target.value })} onChange={(e) => setQuery({...query, jobName: e.target.value})}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()} onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/> />
</div> </div>
<Select <Select
value={query.categoryId?.toString() || 'all'} value={query.categoryId?.toString() || 'all'}
onValueChange={(value) => setQuery({ ...query, categoryId: value !== 'all' ? Number(value) : undefined, pageNum: 0 })} onValueChange={(value) => setQuery({...query, categoryId: value !== 'all' ? Number(value) : undefined, pageNum: 0})}
> >
<SelectTrigger className="w-[200px]"> <SelectTrigger className="w-[200px]">
<SelectValue placeholder="全部分类" /> <SelectValue placeholder="全部分类"/>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all"></SelectItem> <SelectItem value="all"></SelectItem>
@ -372,10 +410,10 @@ const ScheduleJobList: React.FC = () => {
</Select> </Select>
<Select <Select
value={query.status || 'all'} value={query.status || 'all'}
onValueChange={(value) => setQuery({ ...query, status: value !== 'all' ? value as JobStatus : undefined, pageNum: 0 })} onValueChange={(value) => setQuery({...query, status: value !== 'all' ? value as JobStatus : undefined, pageNum: 0})}
> >
<SelectTrigger className="w-[150px]"> <SelectTrigger className="w-[150px]">
<SelectValue placeholder="全部状态" /> <SelectValue placeholder="全部状态"/>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all"></SelectItem> <SelectItem value="all"></SelectItem>
@ -385,7 +423,7 @@ const ScheduleJobList: React.FC = () => {
</SelectContent> </SelectContent>
</Select> </Select>
<Button onClick={handleSearch}> <Button onClick={handleSearch}>
<Search className="h-4 w-4 mr-2" /> <Search className="h-4 w-4 mr-2"/>
</Button> </Button>
<Button variant="outline" onClick={handleReset}> <Button variant="outline" onClick={handleReset}>
@ -416,7 +454,7 @@ const ScheduleJobList: React.FC = () => {
<TableRow> <TableRow>
<TableCell colSpan={11} className="h-24 text-center"> <TableCell colSpan={11} className="h-24 text-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin"/>
<span className="text-muted-foreground">...</span> <span className="text-muted-foreground">...</span>
</div> </div>
</TableCell> </TableCell>
@ -457,18 +495,12 @@ const ScheduleJobList: React.FC = () => {
<span className="text-muted-foreground">-</span> <span className="text-muted-foreground">-</span>
)} )}
</TableCell> </TableCell>
<TableCell sticky width="240px"> <TableCell sticky width="280px">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button {/* ENABLED 按钮控制 */}
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleTrigger(record)}
title="立即触发"
>
<PlayCircle className="h-4 w-4" />
</Button>
{record.status === 'ENABLED' && ( {record.status === 'ENABLED' && (
<>
{/* 暂停 */}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -476,10 +508,34 @@ const ScheduleJobList: React.FC = () => {
onClick={() => handlePause(record)} onClick={() => handlePause(record)}
title="暂停" title="暂停"
> >
<Pause className="h-4 w-4" /> <Pause className="h-4 w-4"/>
</Button> </Button>
{/* 禁用 */}
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:text-orange-500 dark:hover:bg-orange-950/20"
onClick={() => handleDisable(record)}
title="禁用"
>
<Ban className="h-4 w-4"/>
</Button>
{/* 立即执行 */}
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-green-600 hover:text-green-700 hover:bg-green-50 dark:text-green-500 dark:hover:bg-green-950/20"
onClick={() => handleTrigger(record)}
title="立即执行"
>
<PlayCircle className="h-4 w-4"/>
</Button>
</>
)} )}
{/* PAUSED 按钮控制 */}
{record.status === 'PAUSED' && ( {record.status === 'PAUSED' && (
<>
{/* 恢复 */}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -487,9 +543,42 @@ const ScheduleJobList: React.FC = () => {
onClick={() => handleResume(record)} onClick={() => handleResume(record)}
title="恢复" title="恢复"
> >
<Play className="h-4 w-4" /> <Play className="h-4 w-4"/>
</Button>
{/* 禁用 */}
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-orange-600 hover:text-orange-700 hover:bg-orange-50 dark:text-orange-500 dark:hover:bg-orange-950/20"
onClick={() => handleDisable(record)}
title="禁用"
>
<Ban className="h-4 w-4"/>
</Button>
</>
)}
{/* DISABLED 按钮控制 */}
{record.status === 'DISABLED' && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950/20"
onClick={() => handleEnable(record)}
title="启用"
>
<CheckCircle2 className="h-4 w-4"/>
</Button> </Button>
)} )}
{/* 删除按钮 所有状态显示 */}
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleDeleteClick(record)}
title="删除"
>
<Trash2 className="h-4 w-4"/>
</Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -497,7 +586,7 @@ const ScheduleJobList: React.FC = () => {
onClick={() => handleViewLog(record)} onClick={() => handleViewLog(record)}
title="查看日志" title="查看日志"
> >
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4"/>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@ -506,16 +595,7 @@ const ScheduleJobList: React.FC = () => {
onClick={() => handleEdit(record)} onClick={() => handleEdit(record)}
title="编辑" title="编辑"
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4"/>
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleDeleteClick(record)}
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
</TableCell> </TableCell>
@ -539,7 +619,7 @@ const ScheduleJobList: React.FC = () => {
pageIndex={query.pageNum || 0} pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE} pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={data.totalPages} pageCount={data.totalPages}
onPageChange={(pageIndex) => setQuery({ ...query, pageNum: pageIndex })} onPageChange={(pageIndex) => setQuery({...query, pageNum: pageIndex})}
/> />
</div> </div>
)} )}
@ -623,7 +703,7 @@ const ScheduleJobList: React.FC = () => {
{/* 仪表盘视图 */} {/* 仪表盘视图 */}
<TabsContent value="dashboard"> <TabsContent value="dashboard">
<Dashboard /> <Dashboard/>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@ -122,6 +122,17 @@ export const stopJob = (id: number) =>
export const triggerJob = (id: number) => export const triggerJob = (id: number) =>
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/trigger`); request.post<void>(`${SCHEDULE_JOB_URL}/${id}/trigger`);
/**
*
*/
export const disableJob = (id: number) =>
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/disable`);
/**
*
*/
export const enableJob = (id: number) => request.post<void>(`${SCHEDULE_JOB_URL}/${id}/enable`);
/** /**
* Cron表达式 * Cron表达式
*/ */