增加审批组件

This commit is contained in:
dengqichen 2025-10-29 22:02:37 +08:00
parent 7c49715172
commit ffbad526ac
4 changed files with 924 additions and 672 deletions

View File

@ -118,12 +118,15 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
pageCount, pageCount,
onPageChange, onPageChange,
}) => { }) => {
// 将 0-based 的 pageIndex 转换为 1-based 的显示页码
const currentPage = pageIndex + 1;
const renderPageNumbers = () => { const renderPageNumbers = () => {
const pages = []; const pages = [];
const maxVisiblePages = 5; const maxVisiblePages = 5;
const halfMaxVisiblePages = Math.floor(maxVisiblePages / 2); const halfMaxVisiblePages = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, pageIndex - halfMaxVisiblePages); let startPage = Math.max(1, currentPage - halfMaxVisiblePages);
let endPage = Math.min(pageCount, startPage + maxVisiblePages - 1); let endPage = Math.min(pageCount, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) { if (endPage - startPage + 1 < maxVisiblePages) {
@ -134,7 +137,12 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
if (startPage > 1) { if (startPage > 1) {
pages.push( pages.push(
<PaginationItem key={1}> <PaginationItem key={1}>
<PaginationLink onClick={() => onPageChange(1)}>1</PaginationLink> <PaginationLink
isActive={currentPage === 1}
onClick={() => onPageChange(0)}
>
1
</PaginationLink>
</PaginationItem> </PaginationItem>
); );
if (startPage > 2) { if (startPage > 2) {
@ -151,8 +159,8 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
pages.push( pages.push(
<PaginationItem key={i}> <PaginationItem key={i}>
<PaginationLink <PaginationLink
isActive={i === pageIndex} isActive={i === currentPage}
onClick={() => onPageChange(i)} onClick={() => onPageChange(i - 1)}
> >
{i} {i}
</PaginationLink> </PaginationLink>
@ -171,7 +179,10 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
} }
pages.push( pages.push(
<PaginationItem key={pageCount}> <PaginationItem key={pageCount}>
<PaginationLink onClick={() => onPageChange(pageCount)}> <PaginationLink
isActive={currentPage === pageCount}
onClick={() => onPageChange(pageCount - 1)}
>
{pageCount} {pageCount}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
@ -186,15 +197,15 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<PaginationPrevious <PaginationPrevious
onClick={() => onPageChange(Math.max(1, pageIndex - 1))} onClick={() => onPageChange(Math.max(0, pageIndex - 1))}
className={cn(pageIndex <= 1 && "pointer-events-none opacity-50")} className={cn(pageIndex <= 0 && "pointer-events-none opacity-50")}
/> />
</PaginationItem> </PaginationItem>
{renderPageNumbers()} {renderPageNumbers()}
<PaginationItem> <PaginationItem>
<PaginationNext <PaginationNext
onClick={() => onPageChange(Math.min(pageCount, pageIndex + 1))} onClick={() => onPageChange(Math.min(pageCount - 1, pageIndex + 1))}
className={cn(pageIndex >= pageCount && "pointer-events-none opacity-50")} className={cn(pageIndex >= pageCount - 1 && "pointer-events-none opacity-50")}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>

View File

@ -344,9 +344,10 @@ const JenkinsManager: React.FC = () => {
}; };
return ( return (
<div className="p-6 flex flex-col h-screen"> <TooltipProvider>
<div className="flex flex-col h-full p-6 gap-4">
{/* 页面标题 */} {/* 页面标题 */}
<div className="flex items-center justify-between mb-6 flex-shrink-0"> <div className="flex items-center justify-between flex-shrink-0">
<div> <div>
<h1 className="text-3xl font-bold text-foreground">Jenkins </h1> <h1 className="text-3xl font-bold text-foreground">Jenkins </h1>
<p className="text-muted-foreground mt-2"> <p className="text-muted-foreground mt-2">
@ -403,7 +404,7 @@ const JenkinsManager: React.FC = () => {
</div> </div>
</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.views ? ( {loading.views ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
@ -434,26 +435,24 @@ const JenkinsManager: React.FC = () => {
</Badge> </Badge>
)} )}
{view.viewUrl && ( {view.viewUrl && (
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <a
<a href={view.viewUrl}
href={view.viewUrl} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" onClick={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()} className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" >
> <Button variant="ghost" size="icon" className="h-5 w-5">
<Button variant="ghost" size="icon" className="h-5 w-5"> <ExternalLink className="h-3 w-3" />
<ExternalLink className="h-3 w-3" /> </Button>
</Button> </a>
</a> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>
<TooltipContent> <p> Jenkins </p>
<p> Jenkins </p> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
)} )}
</div> </div>
{view.description && ( {view.description && (
@ -532,26 +531,24 @@ const JenkinsManager: React.FC = () => {
</Badge> </Badge>
)} )}
{job.jobUrl && ( {job.jobUrl && (
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <a
<a href={job.jobUrl}
href={job.jobUrl} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" onClick={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()} className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" >
> <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> Jenkins </p>
<p> Jenkins </p> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
)} )}
</div> </div>
@ -564,25 +561,23 @@ const JenkinsManager: React.FC = () => {
<div className="flex items-center gap-3 text-xs"> <div className="flex items-center gap-3 text-xs">
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)} {job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)}
{job.healthReportScore !== undefined && ( {job.healthReportScore !== undefined && (
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <div className="flex items-center gap-1">
<div className="flex items-center gap-1"> <Activity className="h-3 w-3 text-muted-foreground" />
<Activity className="h-3 w-3 text-muted-foreground" /> <Progress
<Progress value={job.healthReportScore}
value={job.healthReportScore} className="w-16 h-2"
className="w-16 h-2" />
/> <span className="text-muted-foreground">
<span className="text-muted-foreground"> {job.healthReportScore}%
{job.healthReportScore}% </span>
</span> </div>
</div> </TooltipTrigger>
</TooltipTrigger> <TooltipContent>
<TooltipContent> : {job.healthReportScore}%
: {job.healthReportScore}% </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
)} )}
</div> </div>
@ -658,25 +653,23 @@ const JenkinsManager: React.FC = () => {
{getBuildStatusBadge(build.buildStatus)} {getBuildStatusBadge(build.buildStatus)}
</div> </div>
{build.buildUrl && ( {build.buildUrl && (
<TooltipProvider> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <a
<a href={build.buildUrl}
href={build.buildUrl} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" >
> <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> Jenkins </p>
<p> Jenkins </p> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip>
</TooltipProvider>
)} )}
</div> </div>
@ -701,6 +694,7 @@ const JenkinsManager: React.FC = () => {
</Card> </Card>
</div> </div>
</div> </div>
</TooltipProvider>
); );
}; };

View File

@ -23,7 +23,7 @@ import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { createScheduleJob, updateScheduleJob } from '../service'; import { createScheduleJob, updateScheduleJob, updateJobCron } from '../service';
import type { ScheduleJobResponse, ScheduleJobRequest, JobCategoryResponse } from '../types'; import type { ScheduleJobResponse, ScheduleJobRequest, JobCategoryResponse } from '../types';
interface JobEditDialogProps { interface JobEditDialogProps {
@ -46,6 +46,8 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
}) => { }) => {
const { toast } = useToast(); const { toast } = useToast();
const [submitting, setSubmitting] = React.useState(false); const [submitting, setSubmitting] = React.useState(false);
// 保存原始的 Cron 表达式,用于判断是否需要更新调度器
const [originalCronExpression, setOriginalCronExpression] = React.useState<string>('');
const form = useForm<ScheduleJobRequest>({ const form = useForm<ScheduleJobRequest>({
defaultValues: { defaultValues: {
@ -69,7 +71,8 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
useEffect(() => { useEffect(() => {
if (open) { if (open) {
if (job) { if (job) {
// 编辑模式 // 编辑模式 - 保存原始的 Cron 表达式
setOriginalCronExpression(job.cronExpression);
form.reset({ form.reset({
jobName: job.jobName, jobName: job.jobName,
jobDescription: job.jobDescription || '', jobDescription: job.jobDescription || '',
@ -86,7 +89,8 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
alertEmail: job.alertEmail || '', alertEmail: job.alertEmail || '',
}); });
} else { } else {
// 新建模式 // 新建模式 - 重置原始 Cron 表达式
setOriginalCronExpression('');
form.reset({ form.reset({
jobName: '', jobName: '',
jobDescription: '', jobDescription: '',
@ -110,11 +114,35 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
setSubmitting(true); setSubmitting(true);
try { try {
if (job) { if (job) {
// 更新任务
await updateScheduleJob(job.id, values); await updateScheduleJob(job.id, values);
toast({
title: '更新成功', // 检查 Cron 表达式是否变化
description: `任务 "${values.jobName}" 已更新`, const cronChanged = values.cronExpression !== originalCronExpression;
});
if (cronChanged) {
// Cron 表达式变化了,需要额外调用更新 Cron 的接口
try {
await updateJobCron(job.id, values.cronExpression);
toast({
title: '更新成功',
description: `任务 "${values.jobName}" 已更新,调度器已重新配置`,
});
} catch (cronError) {
// Cron 更新失败,提示用户
toast({
variant: 'destructive',
title: '调度器更新失败',
description: '任务已保存,但调度器配置更新失败,请重试或联系管理员',
});
console.error('更新 Cron 失败:', cronError);
}
} else {
toast({
title: '更新成功',
description: `任务 "${values.jobName}" 已更新`,
});
}
} else { } else {
await createScheduleJob(values); await createScheduleJob(values);
toast({ toast({

File diff suppressed because it is too large Load Diff