增加审批组件

This commit is contained in:
dengqichen 2025-10-24 20:25:20 +08:00
parent d131634b49
commit 6acc7f339f
10 changed files with 2357 additions and 1512 deletions

View File

@ -0,0 +1,109 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { AlertCircle } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { deleteApplication } from '../service';
import type { Application, DevelopmentLanguageTypeEnum } from '../types';
interface DeleteDialogProps {
open: boolean;
record?: Application;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
const getLanguageLabel = (language: DevelopmentLanguageTypeEnum) => {
const languageMap = {
JAVA: 'Java',
NODE_JS: 'NodeJS',
PYTHON: 'Python',
GO: 'Go',
};
return languageMap[language] || language;
};
/**
*
*/
const DeleteDialog: React.FC<DeleteDialogProps> = ({
open,
record,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const handleConfirm = async () => {
if (!record) return;
try {
await deleteApplication(record.id);
toast({ title: '删除成功', description: `应用 "${record.appName}" 已删除` });
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
if (!record) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
</DialogTitle>
<DialogDescription className="space-y-3 pt-4">
<p></p>
<div className="bg-muted p-3 rounded-md space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.appCode}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.appName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<Badge variant="outline">{getLanguageLabel(record.language)}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.projectGroup?.projectGroupName || '-'}</span>
</div>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="destructive" onClick={handleConfirm}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteDialog;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,103 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { AlertCircle } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { deleteEnvironment } from '../service';
import type { Environment } from '../types';
import { getBuildTypeInfo, getDeployTypeInfo } from '../utils';
interface DeleteDialogProps {
open: boolean;
record?: Environment;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
/**
*
*/
const DeleteDialog: React.FC<DeleteDialogProps> = ({
open,
record,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const handleConfirm = async () => {
if (!record) return;
try {
await deleteEnvironment(record.id);
toast({ title: '删除成功', description: `环境 "${record.envName}" 已删除` });
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
if (!record) return null;
const buildInfo = getBuildTypeInfo(record.buildType);
const deployInfo = getDeployTypeInfo(record.deployType);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
</DialogTitle>
<DialogDescription className="space-y-3 pt-4">
<p></p>
<div className="bg-muted p-3 rounded-md space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.envCode}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.envName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<Badge variant="outline">{buildInfo.label}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<Badge variant="secondary">{deployInfo.label}</Badge>
</div>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="destructive" onClick={handleConfirm}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteDialog;

View File

@ -0,0 +1,220 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useToast } from '@/components/ui/use-toast';
import { createEnvironment, updateEnvironment } from '../service';
import type { Environment, CreateEnvironmentRequest, BuildTypeEnum, DeployTypeEnum } from '../types';
interface EditDialogProps {
open: boolean;
record?: Environment;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
/**
*
*/
const EditDialog: React.FC<EditDialogProps> = ({
open,
record,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const [formData, setFormData] = useState<Partial<CreateEnvironmentRequest>>({
enabled: true,
sort: 1,
});
useEffect(() => {
if (open) {
if (record) {
setFormData({
tenantCode: record.tenantCode,
envCode: record.envCode,
envName: record.envName,
envDesc: record.envDesc,
buildType: record.buildType,
deployType: record.deployType,
sort: record.sort,
enabled: record.enabled,
});
} else {
setFormData({ enabled: true, sort: 1 });
}
}
}, [open, record]);
const handleSubmit = async () => {
try {
// 验证
if (!formData.tenantCode?.trim()) {
toast({ title: '提示', description: '请输入租户代码', variant: 'destructive' });
return;
}
if (!formData.envCode?.trim()) {
toast({ title: '提示', description: '请输入环境编码', variant: 'destructive' });
return;
}
if (!formData.envName?.trim()) {
toast({ title: '提示', description: '请输入环境名称', variant: 'destructive' });
return;
}
if (!formData.buildType) {
toast({ title: '提示', description: '请选择构建类型', variant: 'destructive' });
return;
}
if (!formData.deployType) {
toast({ title: '提示', description: '请选择部署类型', variant: 'destructive' });
return;
}
if (record) {
await updateEnvironment({ ...formData, id: record.id } as any);
toast({ title: '更新成功', description: `环境 "${formData.envName}" 已更新` });
} else {
await createEnvironment(formData as CreateEnvironmentRequest);
toast({ title: '创建成功', description: `环境 "${formData.envName}" 已创建` });
}
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('保存失败:', error);
toast({
title: '保存失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{record ? '编辑环境' : '新建环境'}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="tenantCode"> *</Label>
<Input
id="tenantCode"
value={formData.tenantCode || ''}
onChange={(e) => setFormData({ ...formData, tenantCode: e.target.value })}
placeholder="请输入租户代码"
disabled={!!record}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="envCode"> *</Label>
<Input
id="envCode"
value={formData.envCode || ''}
onChange={(e) => setFormData({ ...formData, envCode: e.target.value })}
placeholder="请输入环境编码"
disabled={!!record}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="envName"> *</Label>
<Input
id="envName"
value={formData.envName || ''}
onChange={(e) => setFormData({ ...formData, envName: e.target.value })}
placeholder="请输入环境名称"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="envDesc"></Label>
<Textarea
id="envDesc"
value={formData.envDesc || ''}
onChange={(e) => setFormData({ ...formData, envDesc: e.target.value })}
placeholder="请输入环境描述"
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="buildType"> *</Label>
<Select
value={formData.buildType}
onValueChange={(value) => setFormData({ ...formData, buildType: value as BuildTypeEnum })}
>
<SelectTrigger>
<SelectValue placeholder="请选择构建类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="JENKINS">Jenkins构建</SelectItem>
<SelectItem value="GITLAB_RUNNER">GitLab Runner构建</SelectItem>
<SelectItem value="GITHUB_ACTION">GitHub Action构建</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="deployType"> *</Label>
<Select
value={formData.deployType}
onValueChange={(value) => setFormData({ ...formData, deployType: value as DeployTypeEnum })}
>
<SelectTrigger>
<SelectValue placeholder="请选择部署类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="K8S">Kubernetes集群部署</SelectItem>
<SelectItem value="DOCKER">Docker容器部署</SelectItem>
<SelectItem value="VM"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="sort"> *</Label>
<Input
id="sort"
type="number"
value={formData.sort || 1}
onChange={(e) => setFormData({ ...formData, sort: Number(e.target.value) })}
placeholder="请输入显示排序"
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="enabled"></Label>
<Switch
id="enabled"
checked={formData.enabled}
onCheckedChange={(checked) => setFormData({ ...formData, enabled: checked })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default EditDialog;

View File

@ -1,245 +1,303 @@
import React, {useState} from 'react';
import {PageContainer} from '@ant-design/pro-components';
import {Button, Space, Popconfirm, Tag, App, Select} from 'antd';
import {PlusOutlined, EditOutlined, DeleteOutlined} from '@ant-design/icons';
import {getEnvironmentPage, deleteEnvironment} from './service';
import type {Environment, EnvironmentQueryParams} from './types';
import {BuildTypeEnum, DeployTypeEnum} from './types';
import {getBuildTypeInfo, getDeployTypeInfo} from './utils';
import EnvironmentModal from './components/EnvironmentModal';
import {ProTable} from '@ant-design/pro-components';
import type {ProColumns, ActionType} from '@ant-design/pro-components';
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@/components/ui/page-container';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useToast } from '@/components/ui/use-toast';
import { DataTablePagination } from '@/components/ui/pagination';
import { Plus, Edit, Trash2, Server, Activity, Database } from 'lucide-react';
import EditDialog from './components/EditDialog';
import DeleteDialog from './components/DeleteDialog';
import { getEnvironmentPage, deleteEnvironment } from './service';
import type { Environment, EnvironmentQueryParams, BuildTypeEnum, DeployTypeEnum } from './types';
import { getBuildTypeInfo, getDeployTypeInfo } from './utils';
const DEFAULT_PAGE_SIZE = 10;
const EnvironmentList: React.FC = () => {
const [modalVisible, setModalVisible] = useState(false);
const [currentEnvironment, setCurrentEnvironment] = useState<Environment>();
const actionRef = React.useRef<ActionType>();
const {message: messageApi} = App.useApp();
const { toast } = useToast();
const [list, setList] = useState<Environment[]>([]);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<EnvironmentQueryParams>({
pageNum: 1,
pageSize: DEFAULT_PAGE_SIZE,
});
const [total, setTotal] = useState(0);
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
const handleDelete = async (id: number) => {
try {
await deleteEnvironment(id);
messageApi.success('删除成功');
actionRef.current?.reload();
} catch (error) {
messageApi.error('删除失败');
}
};
// 对话框状态
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [currentRecord, setCurrentRecord] = useState<Environment>();
const handleAdd = () => {
setCurrentEnvironment(undefined);
setModalVisible(true);
};
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const response = await getEnvironmentPage(query);
if (response) {
setList(response.content || []);
setTotal(response.totalElements || 0);
// 计算统计数据
const all = response.content || [];
setStats({
total: all.length,
enabled: all.filter((item) => item.enabled).length,
disabled: all.filter((item) => !item.enabled).length,
});
}
} catch (error) {
toast({ title: '加载失败', description: '无法加载环境列表', variant: 'destructive' });
} finally {
setLoading(false);
}
};
const handleEdit = (environment: Environment) => {
setCurrentEnvironment(environment);
setModalVisible(true);
};
useEffect(() => {
loadData();
}, [query.pageNum, query.pageSize]);
const handleModalClose = () => {
setModalVisible(false);
setCurrentEnvironment(undefined);
};
// 搜索
const handleSearch = () => {
setQuery({ ...query, pageNum: 1 });
loadData();
};
const handleSuccess = () => {
setModalVisible(false);
setCurrentEnvironment(undefined);
actionRef.current?.reload();
};
// 重置
const handleReset = () => {
setQuery({ pageNum: 1, pageSize: DEFAULT_PAGE_SIZE });
loadData();
};
const columns: ProColumns<Environment>[] = [
{
title: '环境编码',
dataIndex: 'envCode',
width: 120,
copyable: true,
ellipsis: true,
fixed: 'left',
},
{
title: '环境名称',
dataIndex: 'envName',
width: 150,
ellipsis: true,
},
{
title: '环境描述',
dataIndex: 'envDesc',
ellipsis: true,
},
{
title: '构建类型',
dataIndex: 'buildType',
width: 120,
render: (buildType) => {
const typeInfo = getBuildTypeInfo(buildType as BuildTypeEnum);
return (
<Tag color={typeInfo.color}>
<Space>
{typeInfo.icon}
{typeInfo.label}
</Space>
</Tag>
);
},
filters: [
{text: 'Jenkins构建', value: BuildTypeEnum.JENKINS},
{text: 'GitLab Runner构建', value: BuildTypeEnum.GITLAB_RUNNER},
{text: 'GitHub Action构建', value: BuildTypeEnum.GITHUB_ACTION},
],
filterMode: 'menu',
filtered: false,
},
{
title: '部署类型',
dataIndex: 'deployType',
width: 120,
render: (deployType) => {
const typeInfo = getDeployTypeInfo(deployType as DeployTypeEnum);
return (
<Tag color={typeInfo.color}>
<Space>
{typeInfo.icon}
{typeInfo.label}
</Space>
</Tag>
);
},
filters: [
{text: 'Kubernetes集群部署', value: DeployTypeEnum.K8S},
{text: 'Docker容器部署', value: DeployTypeEnum.DOCKER},
{text: '虚拟机部署', value: DeployTypeEnum.VM},
],
filterMode: 'menu',
filtered: false,
},
{
title: '状态',
dataIndex: 'enabled',
width: 100,
valueEnum: {
true: {text: '启用', status: 'Success'},
false: {text: '禁用', status: 'Default'},
},
},
{
title: '排序',
dataIndex: 'sort',
width: 80,
sorter: true,
},
{
title: '操作',
width: 180,
key: 'action',
valueType: 'option',
fixed: 'right',
render: (_, record) => [
<Button
key="edit"
type="link"
onClick={() => handleEdit(record)}
>
<Space>
<EditOutlined/>
</Space>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该环境吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
<Button
type="link"
danger
>
<Space>
<DeleteOutlined/>
</Space>
</Button>
</Popconfirm>
],
},
];
return (
<PageContainer>
{/* 页面标题 */}
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight"></h2>
<Button onClick={() => {
setCurrentRecord(undefined);
setEditDialogOpen(true);
}}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
return (
<PageContainer
header={{
title: '环境管理'
}}
>
<ProTable<Environment>
columns={columns}
actionRef={actionRef}
scroll={{x: 'max-content'}}
cardBordered
rowKey="id"
search={false}
options={{
setting: false,
density: false,
fullScreen: false,
reload: false,
}}
toolbar={{
actions: [
<Button
key="add"
type="primary"
onClick={handleAdd}
icon={<PlusOutlined/>}
>
</Button>
],
}}
form={{
syncToUrl: true,
}}
pagination={{
pageSize: 10,
showQuickJumper: true,
}}
request={async (params) => {
try {
const queryParams: EnvironmentQueryParams = {
pageSize: params.pageSize,
pageNum: params.current,
envCode: params.envCode as string,
envName: params.envName as string,
buildType: params.buildType as BuildTypeEnum,
deployType: params.deployType as DeployTypeEnum,
};
const data = await getEnvironmentPage(queryParams);
return {
data: data.content || [],
success: true,
total: data.totalElements || 0,
};
} catch (error) {
messageApi.error('获取环境列表失败');
return {
data: [],
success: false,
total: 0,
};
}
}}
{/* 统计卡片 */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.enabled}</div>
<p className="text-xs text-muted-foreground">使</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-600">{stats.disabled}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</div>
{/* 搜索过滤 */}
<Card>
<div className="p-6">
<div className="flex items-center gap-4">
<Input
placeholder="环境编码"
value={query.envCode || ''}
onChange={(e) => setQuery({ ...query, envCode: e.target.value })}
className="max-w-[200px]"
/>
<Input
placeholder="环境名称"
value={query.envName || ''}
onChange={(e) => setQuery({ ...query, envName: e.target.value })}
className="max-w-[200px]"
/>
<Select
value={query.buildType}
onValueChange={(value) => setQuery({ ...query, buildType: value as BuildTypeEnum })}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="构建类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="JENKINS">Jenkins构建</SelectItem>
<SelectItem value="GITLAB_RUNNER">GitLab Runner构建</SelectItem>
<SelectItem value="GITHUB_ACTION">GitHub Action构建</SelectItem>
</SelectContent>
</Select>
<Select
value={query.deployType}
onValueChange={(value) => setQuery({ ...query, deployType: value as DeployTypeEnum })}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="部署类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="K8S">Kubernetes集群部署</SelectItem>
<SelectItem value="DOCKER">Docker容器部署</SelectItem>
<SelectItem value="VM"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={handleReset}>
</Button>
<Button onClick={handleSearch}>
</Button>
</div>
</div>
</Card>
{modalVisible && (
<EnvironmentModal
open={modalVisible}
onCancel={handleModalClose}
onSuccess={handleSuccess}
initialValues={currentEnvironment}
/>
)}
</PageContainer>
);
{/* 数据表格 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[200px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[200px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
</TableCell>
</TableRow>
) : (
list.map((item) => {
const buildInfo = getBuildTypeInfo(item.buildType);
const deployInfo = getDeployTypeInfo(item.deployType);
return (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.envCode}</TableCell>
<TableCell>{item.envName}</TableCell>
<TableCell className="text-muted-foreground">{item.envDesc || '-'}</TableCell>
<TableCell>
<Badge variant="outline" className="inline-flex items-center gap-1">
{buildInfo.icon}
{buildInfo.label}
</Badge>
</TableCell>
<TableCell>
<Badge variant="secondary" className="inline-flex items-center gap-1">
{deployInfo.icon}
{deployInfo.label}
</Badge>
</TableCell>
<TableCell>{item.sort}</TableCell>
<TableCell>
<Badge variant={item.enabled ? "default" : "secondary"} className="inline-flex">
{item.enabled ? '启用' : '禁用'}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setCurrentRecord(item);
setEditDialogOpen(true);
}}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setCurrentRecord(item);
setDeleteDialogOpen(true);
}}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</div>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={query.pageNum || 1}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={Math.ceil(total / (query.pageSize || DEFAULT_PAGE_SIZE))}
onPageChange={(page) => setQuery({ ...query, pageNum: page })}
/>
</div>
</div>
</CardContent>
</Card>
{/* 对话框 */}
<EditDialog
open={editDialogOpen}
record={currentRecord}
onOpenChange={setEditDialogOpen}
onSuccess={loadData}
/>
<DeleteDialog
open={deleteDialogOpen}
record={currentRecord}
onOpenChange={setDeleteDialogOpen}
onSuccess={loadData}
/>
</PageContainer>
);
};
export default EnvironmentList;

View File

@ -0,0 +1,104 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { AlertCircle } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { deleteExternalSystem } from '../service';
import type { ExternalSystemResponse, SystemType } from '../types';
interface DeleteDialogProps {
open: boolean;
record?: ExternalSystemResponse;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
const getSystemTypeLabel = (type: SystemType) => {
const typeMap = {
JENKINS: 'Jenkins',
GIT: 'Git',
ZENTAO: '禅道',
};
return typeMap[type];
};
/**
*
*/
const DeleteDialog: React.FC<DeleteDialogProps> = ({
open,
record,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const handleConfirm = async () => {
if (!record) return;
try {
await deleteExternalSystem(record.id);
toast({ title: '删除成功', description: `系统 "${record.name}" 已删除` });
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
if (!record) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
</DialogTitle>
<DialogDescription className="space-y-3 pt-4">
<p></p>
<div className="bg-muted p-3 rounded-md space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.name}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<Badge variant="outline">{getSystemTypeLabel(record.type)}</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-mono text-xs truncate max-w-[250px]">{record.url}</span>
</div>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="destructive" onClick={handleConfirm}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteDialog;

View File

@ -0,0 +1,265 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useToast } from '@/components/ui/use-toast';
import { createExternalSystem, updateExternalSystem } from '../service';
import type { ExternalSystemResponse, ExternalSystemRequest, SystemType, AuthType } from '../types';
interface EditDialogProps {
open: boolean;
record?: ExternalSystemResponse;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
/**
*
*/
const EditDialog: React.FC<EditDialogProps> = ({
open,
record,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const [formData, setFormData] = useState<Partial<ExternalSystemRequest>>({
enabled: true,
sort: 1,
authType: 'BASIC' as AuthType,
});
useEffect(() => {
if (open) {
if (record) {
setFormData({
name: record.name,
type: record.type,
url: record.url,
authType: record.authType,
username: record.username,
password: undefined, // 不显示密码
token: undefined,
sort: record.sort,
remark: record.remark,
enabled: record.enabled,
});
} else {
setFormData({ enabled: true, sort: 1, authType: 'BASIC' as AuthType });
}
}
}, [open, record]);
const handleSubmit = async () => {
try {
// 验证
if (!formData.name?.trim()) {
toast({ title: '提示', description: '请输入系统名称', variant: 'destructive' });
return;
}
if (!formData.type) {
toast({ title: '提示', description: '请选择系统类型', variant: 'destructive' });
return;
}
if (!formData.url?.trim()) {
toast({ title: '提示', description: '请输入系统地址', variant: 'destructive' });
return;
}
// URL 验证
try {
new URL(formData.url);
} catch {
toast({ title: '提示', description: '请输入有效的URL', variant: 'destructive' });
return;
}
if (formData.authType === 'BASIC') {
if (!formData.username?.trim()) {
toast({ title: '提示', description: '请输入用户名', variant: 'destructive' });
return;
}
if (!record && !formData.password) {
toast({ title: '提示', description: '请输入密码', variant: 'destructive' });
return;
}
}
if (formData.authType === 'TOKEN') {
if (!record && !formData.token) {
toast({ title: '提示', description: '请输入访问令牌', variant: 'destructive' });
return;
}
}
if (record) {
await updateExternalSystem(record.id, formData as ExternalSystemRequest);
toast({ title: '更新成功', description: `系统 "${formData.name}" 已更新` });
} else {
await createExternalSystem(formData as ExternalSystemRequest);
toast({ title: '创建成功', description: `系统 "${formData.name}" 已创建` });
}
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('保存失败:', error);
toast({
title: '保存失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{record ? '编辑系统' : '新增系统'}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="请输入系统名称"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="type"> *</Label>
<Select
value={formData.type}
onValueChange={(value) => setFormData({ ...formData, type: value as SystemType })}
>
<SelectTrigger>
<SelectValue placeholder="请选择系统类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="JENKINS">Jenkins</SelectItem>
<SelectItem value="GIT">Git</SelectItem>
<SelectItem value="ZENTAO"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="url"> *</Label>
<Input
id="url"
value={formData.url || ''}
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
placeholder="请输入系统地址"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="authType"> *</Label>
<Select
value={formData.authType}
onValueChange={(value) => setFormData({ ...formData, authType: value as AuthType })}
>
<SelectTrigger>
<SelectValue placeholder="请选择认证方式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="BASIC"></SelectItem>
<SelectItem value="TOKEN"></SelectItem>
<SelectItem value="OAUTH">OAuth2</SelectItem>
</SelectContent>
</Select>
</div>
{formData.authType === 'BASIC' && (
<>
<div className="grid gap-2">
<Label htmlFor="username"> *</Label>
<Input
id="username"
value={formData.username || ''}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
placeholder="请输入用户名"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password"> {!record && '*'}</Label>
<Input
id="password"
type="password"
value={formData.password || ''}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder={record ? '不修改请留空' : '请输入密码'}
/>
</div>
</>
)}
{formData.authType === 'TOKEN' && (
<div className="grid gap-2">
<Label htmlFor="token">访 {!record && '*'}</Label>
<Input
id="token"
type="password"
value={formData.token || ''}
onChange={(e) => setFormData({ ...formData, token: e.target.value })}
placeholder={record ? '不修改请留空' : '请输入访问令牌'}
/>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="sort"> *</Label>
<Input
id="sort"
type="number"
value={formData.sort || 1}
onChange={(e) => setFormData({ ...formData, sort: Number(e.target.value) })}
placeholder="请输入显示排序"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="remark"></Label>
<Textarea
id="remark"
value={formData.remark || ''}
onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
placeholder="请输入备注"
rows={4}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="enabled"></Label>
<Switch
id="enabled"
checked={formData.enabled}
onCheckedChange={(checked) => setFormData({ ...formData, enabled: checked })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSubmit}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default EditDialog;

View File

@ -1,344 +1,350 @@
import React, {useState} from 'react';
import {Card, Table, Button, Space, Modal, Form, Input, message, Select, InputNumber, Switch, Tag, Tooltip} from 'antd';
import {PlusOutlined, EditOutlined, DeleteOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, LinkOutlined, MinusCircleOutlined} from '@ant-design/icons';
import type {ColumnsType} from 'antd/es/table';
import {useTableData} from '@/hooks/useTableData';
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@/components/ui/page-container';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { DataTablePagination } from '@/components/ui/pagination';
import { Plus, Edit, Trash2, Link, Server, Activity, Database } from 'lucide-react';
import EditDialog from './components/EditDialog';
import DeleteDialog from './components/DeleteDialog';
import * as service from './service';
import {SystemType, AuthType, SyncStatus, ExternalSystemResponse, ExternalSystemRequest, ExternalSystemQuery} from './types';
import type { ExternalSystemResponse, ExternalSystemQuery, SystemType, AuthType } from './types';
const DEFAULT_PAGE_SIZE = 10;
const ExternalPage: React.FC = () => {
const [form] = Form.useForm();
const [modalVisible, setModalVisible] = useState(false);
const [editingSystem, setEditingSystem] = useState<ExternalSystemResponse | null>(null);
const { toast } = useToast();
const [list, setList] = useState<ExternalSystemResponse[]>([]);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<ExternalSystemQuery>({
pageNum: 1,
pageSize: DEFAULT_PAGE_SIZE,
});
const [total, setTotal] = useState(0);
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
const {
list,
loading,
pagination,
handleTableChange,
handleCreate,
handleUpdate,
handleDelete,
refresh
} = useTableData<ExternalSystemResponse, ExternalSystemQuery, ExternalSystemRequest, ExternalSystemRequest>({
service: {
list: service.getExternalSystemsPage,
create: service.createExternalSystem,
update: service.updateExternalSystem,
delete: service.deleteExternalSystem
},
config: {
message: {
createSuccess: '创建系统成功',
updateSuccess: '更新系统成功',
deleteSuccess: '删除系统成功'
}
}
});
// 对话框状态
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [currentRecord, setCurrentRecord] = useState<ExternalSystemResponse>();
const handleAdd = () => {
setEditingSystem(null);
form.resetFields();
form.setFieldsValue({
enabled: true,
sort: 1,
authType: AuthType.BASIC
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const response = await service.getExternalSystemsPage(query);
if (response) {
setList(response.content || []);
setTotal(response.totalElements || 0);
// 计算统计数据
const all = response.content || [];
setStats({
total: all.length,
enabled: all.filter((item) => item.enabled).length,
disabled: all.filter((item) => !item.enabled).length,
});
setModalVisible(true);
}
} catch (error) {
toast({ title: '加载失败', description: '无法加载外部系统列表', variant: 'destructive' });
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [query.pageNum, query.pageSize]);
// 搜索
const handleSearch = () => {
setQuery({ ...query, pageNum: 1 });
loadData();
};
// 重置
const handleReset = () => {
setQuery({ pageNum: 1, pageSize: DEFAULT_PAGE_SIZE });
loadData();
};
// 测试连接
const handleTestConnection = async (id: number) => {
try {
const success = await service.testConnection(id);
toast({
title: success ? '连接成功' : '连接失败',
description: success ? '外部系统连接正常' : '无法连接到外部系统',
variant: success ? 'default' : 'destructive',
});
} catch (error) {
toast({ title: '测试失败', description: '无法测试连接', variant: 'destructive' });
}
};
// 切换状态
const handleStatusChange = async (id: number, enabled: boolean) => {
try {
await service.updateStatus(id, enabled);
toast({ title: '更新成功', description: `系统状态已${enabled ? '启用' : '禁用'}` });
loadData();
} catch (error) {
toast({ title: '更新失败', description: '无法更新系统状态', variant: 'destructive' });
}
};
// 获取系统类型标签
const getSystemTypeLabel = (type: SystemType) => {
const typeMap = {
JENKINS: { label: 'Jenkins', variant: 'default' as const },
GIT: { label: 'Git', variant: 'secondary' as const },
ZENTAO: { label: '禅道', variant: 'outline' as const },
};
return typeMap[type] || { label: type, variant: 'outline' as const };
};
const handleEdit = (record: ExternalSystemResponse) => {
setEditingSystem(record);
form.setFieldsValue({
...record,
password: undefined // 不显示密码
});
setModalVisible(true);
// 获取认证方式标签
const getAuthTypeLabel = (authType: AuthType) => {
const authTypeMap = {
BASIC: '用户名密码',
TOKEN: '令牌',
OAUTH: 'OAuth2',
};
return authTypeMap[authType];
};
const handleTestConnection = async (id: number) => {
try {
const success = await service.testConnection(id);
message.success(success ? '连接成功' : '连接失败');
} catch (error) {
message.error('测试连接失败');
}
};
return (
<PageContainer>
{/* 页面标题 */}
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight"></h2>
<Button onClick={() => {
setCurrentRecord(undefined);
setEditDialogOpen(true);
}}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
const handleStatusChange = async (id: number, enabled: boolean) => {
try {
await service.updateStatus(id, enabled);
message.success('更新状态成功');
refresh();
} catch (error) {
message.error('更新状态失败');
}
};
{/* 统计卡片 */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.enabled}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-600">{stats.disabled}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</div>
const handleSubmit = async () => {
try {
const values = await form.validateFields();
let success = false;
if (editingSystem) {
success = await handleUpdate(editingSystem.id, values);
} else {
success = await handleCreate(values);
}
if (success) {
setModalVisible(false);
}
} catch (error: any) {
// 如果是表单验证错误,不显示错误消息
if (!error.errorFields) {
// 如果是后端返回的错误,显示后端的错误消息
if (error.response?.data) {
message.error(error.response.data.message || '操作失败');
} else {
message.error(error.message || '操作失败');
}
}
}
};
const columns: ColumnsType<ExternalSystemResponse> = [
{
title: '系统名称',
dataIndex: 'name',
width: 200,
},
{
title: '系统类型',
dataIndex: 'type',
width: 120,
render: (type: SystemType) => {
const typeMap = {
[SystemType.JENKINS]: 'Jenkins',
[SystemType.GIT]: 'Git',
[SystemType.ZENTAO]: '禅道'
};
return typeMap[type];
}
},
{
title: '系统地址',
dataIndex: 'url',
width: 300,
render: (url: string) => (
<a href={url} target="_blank" rel="noopener noreferrer">
{url}
</a>
),
},
{
title: '认证方式',
dataIndex: 'authType',
width: 120,
render: (authType: AuthType) => {
const authTypeMap = {
[AuthType.BASIC]: '用户名密码',
[AuthType.TOKEN]: '令牌',
[AuthType.OAUTH]: 'OAuth2'
};
return authTypeMap[authType];
}
},
{
title: '最后连接时间',
dataIndex: 'lastConnectTime',
width: 150,
render: (time: string) => time || '-'
},
{
title: '状态',
dataIndex: 'enabled',
width: 100,
render: (enabled: boolean, record) => (
<Switch
checked={enabled}
onChange={(checked) => handleStatusChange(record.id, checked)}
checkedChildren="否"
unCheckedChildren="是"
/>
)
},
{
title: '操作',
key: 'action',
width: 280,
render: (_, record) => (
<Space>
<Button
type="link"
icon={<EditOutlined/>}
onClick={() => handleEdit(record)}
>
</Button>
<Button
type="link"
icon={<LinkOutlined/>}
onClick={() => handleTestConnection(record.id)}
>
</Button>
<Button
type="link"
danger
icon={<DeleteOutlined/>}
onClick={() => handleDelete(record.id)}
>
</Button>
</Space>
),
},
];
return (
<div>
<Card>
<div style={{marginBottom: 16}}>
<Button
type="primary"
icon={<PlusOutlined/>}
onClick={handleAdd}
>
</Button>
</div>
<Table
columns={columns}
dataSource={list}
rowKey="id"
loading={loading}
pagination={pagination}
onChange={handleTableChange}
/>
</Card>
<Modal
title={editingSystem ? '编辑系统' : '新增系统'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={600}
{/* 搜索过滤 */}
<Card>
<div className="p-6">
<div className="flex items-center gap-4">
<Input
placeholder="系统名称"
value={query.name || ''}
onChange={(e) => setQuery({ ...query, name: e.target.value })}
className="max-w-[200px]"
/>
<Select
value={query.type}
onValueChange={(value) => setQuery({ ...query, type: value as SystemType })}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="name"
label="系统名称"
rules={[{required: true, message: '请输入系统名称'}]}
>
<Input placeholder="请输入系统名称"/>
</Form.Item>
<Form.Item
name="type"
label="系统类型"
rules={[{required: true, message: '请选择系统类型'}]}
>
<Select>
<Select.Option value={SystemType.JENKINS}>Jenkins</Select.Option>
<Select.Option value={SystemType.GIT}>Git</Select.Option>
<Select.Option value={SystemType.ZENTAO}></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="url"
label="系统地址"
rules={[
{required: true, message: '请输入系统地址'},
{type: 'url', message: '请输入有效的URL'}
]}
>
<Input placeholder="请输入系统地址"/>
</Form.Item>
<Form.Item
name="authType"
label="认证方式"
rules={[{required: true, message: '请选择认证方式'}]}
>
<Select>
<Select.Option value={AuthType.BASIC}></Select.Option>
<Select.Option value={AuthType.TOKEN}></Select.Option>
<Select.Option value={AuthType.OAUTH}>OAuth2</Select.Option>
</Select>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.authType !== currentValues.authType}
>
{({getFieldValue}) => {
const authType = getFieldValue('authType');
if (authType === AuthType.BASIC) {
return (
<>
<Form.Item
name="username"
label="用户名"
rules={[{required: true, message: '请输入用户名'}]}
>
<Input placeholder="请输入用户名"/>
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{required: !editingSystem, message: '请输入密码'}]}
>
<Input.Password placeholder={editingSystem ? '不修改请留空' : '请输入密码'}/>
</Form.Item>
</>
);
}
if (authType === AuthType.TOKEN) {
return (
<Form.Item
name="token"
label="访问令牌"
rules={[{required: !editingSystem, message: '请输入访问令牌'}]}
>
<Input.Password placeholder={editingSystem ? '不修改请留空' : '请输入访问令牌'}/>
</Form.Item>
);
}
return null;
}}
</Form.Item>
<Form.Item
name="sort"
label="显示排序"
rules={[{required: true, message: '请输入显示排序'}]}
>
<InputNumber style={{width: '100%'}} min={0} placeholder="请输入显示排序"/>
</Form.Item>
<Form.Item
name="remark"
label="备注"
>
<Input.TextArea rows={4} placeholder="请输入备注"/>
</Form.Item>
<Form.Item
name="enabled"
label="是否禁用"
valuePropName="checked"
>
<Switch checkedChildren="否" unCheckedChildren="是"/>
</Form.Item>
</Form>
</Modal>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="系统类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="JENKINS">Jenkins</SelectItem>
<SelectItem value="GIT">Git</SelectItem>
<SelectItem value="ZENTAO"></SelectItem>
</SelectContent>
</Select>
<Select
value={query.enabled?.toString()}
onValueChange={(value) => setQuery({ ...query, enabled: value === 'true' })}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={handleReset}>
</Button>
<Button onClick={handleSearch}>
</Button>
</div>
</div>
);
</Card>
{/* 数据表格 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[250px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[250px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
</TableCell>
</TableRow>
) : (
list.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>
<Badge variant={getSystemTypeLabel(item.type).variant}>
{getSystemTypeLabel(item.type).label}
</Badge>
</TableCell>
<TableCell>
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-1"
>
<Link className="h-3 w-3" />
{item.url}
</a>
</TableCell>
<TableCell>{getAuthTypeLabel(item.authType)}</TableCell>
<TableCell className="text-muted-foreground">
{item.lastConnectTime || '-'}
</TableCell>
<TableCell>
<Switch
checked={item.enabled}
onCheckedChange={(checked) => handleStatusChange(item.id, checked)}
/>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setCurrentRecord(item);
setEditDialogOpen(true);
}}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleTestConnection(item.id)}
>
<Link className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setCurrentRecord(item);
setDeleteDialogOpen(true);
}}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={query.pageNum || 1}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={Math.ceil(total / (query.pageSize || DEFAULT_PAGE_SIZE))}
onPageChange={(page) => setQuery({ ...query, pageNum: page })}
/>
</div>
</div>
</CardContent>
</Card>
{/* 对话框 */}
<EditDialog
open={editDialogOpen}
record={currentRecord}
onOpenChange={setEditDialogOpen}
onSuccess={loadData}
/>
<DeleteDialog
open={deleteDialogOpen}
record={currentRecord}
onOpenChange={setDeleteDialogOpen}
onSuccess={loadData}
/>
</PageContainer>
);
};
export default ExternalPage;
export default ExternalPage;

View File

@ -0,0 +1,109 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { AlertCircle } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { deleteProjectGroup } from '../service';
import type { ProjectGroup } from '../types';
import { getProjectTypeInfo } from '../utils';
interface DeleteDialogProps {
open: boolean;
record?: ProjectGroup;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
/**
*
*/
const DeleteDialog: React.FC<DeleteDialogProps> = ({
open,
record,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const handleConfirm = async () => {
if (!record) return;
try {
await deleteProjectGroup(record.id);
toast({ title: '删除成功', description: `项目组 "${record.projectGroupName}" 已删除` });
onSuccess();
onOpenChange(false);
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
if (!record) return null;
const typeInfo = getProjectTypeInfo(record.type);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
</DialogTitle>
<DialogDescription className="space-y-3 pt-4">
<p></p>
<div className="bg-muted p-3 rounded-md space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.projectGroupCode}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.projectGroupName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<Badge variant="outline" className="inline-flex items-center gap-1">
{typeInfo.icon}
{typeInfo.label}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.totalEnvironments}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{record.totalApplications}</span>
</div>
</div>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button variant="destructive" onClick={handleConfirm}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteDialog;

View File

@ -1,497 +1,346 @@
import React, {useState, useEffect} from 'react';
import {PageContainer} from '@/components/ui/page-container';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
TeamOutlined,
EnvironmentOutlined,
SearchOutlined,
} from '@ant-design/icons';
import {getProjectGroupPage, deleteProjectGroup} from './service';
import type {ProjectGroup, ProjectGroupQueryParams} from './types';
import {ProjectGroupTypeEnum} from './types';
import {getProjectTypeInfo} from './utils';
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@/components/ui/page-container';
import { getProjectGroupPage } from './service';
import type { ProjectGroup, ProjectGroupQueryParams } from './types';
import { ProjectGroupTypeEnum } from './types';
import { getProjectTypeInfo } from './utils';
import ProjectGroupModal from './components/ProjectGroupModal';
import DeleteDialog from './components/DeleteDialog';
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from "@/components/ui/table";
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from '@/components/ui/table';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {Button} from "@/components/ui/button";
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {Badge} from "@/components/ui/badge";
import {useToast} from "@/components/ui/use-toast";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import {searchFormSchema, type SearchFormValues} from "./schema";
import {DataTablePagination} from "@/components/ui/pagination";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { searchFormSchema, type SearchFormValues } from './schema';
import { DataTablePagination } from '@/components/ui/pagination';
import { Plus, Edit, Trash2, FolderKanban, Activity, Database, Layers, Boxes } from 'lucide-react';
interface Column {
accessorKey?: keyof ProjectGroup;
id?: string;
header: string;
size: number;
cell?: (props: { row: { original: ProjectGroup } }) => React.ReactNode;
}
const DEFAULT_PAGE_SIZE = 10;
const ProjectGroupList: React.FC = () => {
const [modalVisible, setModalVisible] = useState(false);
const [currentProject, setCurrentProject] = useState<ProjectGroup>();
const [list, setList] = useState<ProjectGroup[]>([]);
const [loading, setLoading] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState<Record<string | number, boolean>>({});
const [pagination, setPagination] = useState({
pageNum: 1,
pageSize: 10,
totalElements: 0,
});
const {toast} = useToast();
const [modalVisible, setModalVisible] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [currentProject, setCurrentProject] = useState<ProjectGroup>();
const [list, setList] = useState<ProjectGroup[]>([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
pageNum: 1,
pageSize: DEFAULT_PAGE_SIZE,
totalElements: 0,
});
const [stats, setStats] = useState({ total: 0, enabled: 0, disabled: 0 });
const { toast } = useToast();
const form = useForm<SearchFormValues>({
resolver: zodResolver(searchFormSchema),
defaultValues: {
projectGroupCode: "",
projectGroupName: "",
type: undefined,
enabled: undefined,
},
});
const form = useForm<SearchFormValues>({
resolver: zodResolver(searchFormSchema),
defaultValues: {
projectGroupCode: '',
projectGroupName: '',
type: undefined,
enabled: undefined,
},
});
const loadData = async (params?: SearchFormValues) => {
setLoading(true);
try {
const queryParams: ProjectGroupQueryParams = {
...params,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
};
const data = await getProjectGroupPage(queryParams);
setList(data.content || []);
setPagination(prev => ({
...prev,
totalElements: data.totalElements,
}));
} catch (error) {
toast({
variant: "destructive",
title: "加载失败",
description: "加载数据失败,请稍后重试",
duration: 3000,
});
} finally {
setLoading(false);
}
};
const loadData = async (params?: SearchFormValues) => {
setLoading(true);
try {
const queryParams: ProjectGroupQueryParams = {
...params,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
};
const data = await getProjectGroupPage(queryParams);
setList(data.content || []);
setPagination((prev) => ({
...prev,
totalElements: data.totalElements,
}));
// 计算统计数据
const all = data.content || [];
setStats({
total: all.length,
enabled: all.filter((item) => item.enabled).length,
disabled: all.filter((item) => !item.enabled).length,
});
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '加载数据失败,请稍后重试',
duration: 3000,
});
} finally {
setLoading(false);
}
};
const handlePageChange = (page: number) => {
setPagination(prev => ({
...prev,
pageNum: page,
}));
};
const handlePageChange = (page: number) => {
setPagination((prev) => ({
...prev,
pageNum: page,
}));
};
useEffect(() => {
loadData(form.getValues());
}, [pagination.pageNum, pagination.pageSize]);
useEffect(() => {
loadData(form.getValues());
}, [pagination.pageNum, pagination.pageSize]);
const handleDelete = async (id: number) => {
try {
await deleteProjectGroup(id);
toast({
variant: "default",
title: "操作成功",
description: "项目组删除成功",
duration: 3000,
});
loadData(form.getValues());
setDeleteDialogOpen(prev => ({...prev, [id]: false}));
} catch (error) {
toast({
variant: "destructive",
title: "操作失败",
description: "删除项目组失败,请稍后重试",
duration: 3000,
});
}
};
const handleEdit = (project: ProjectGroup) => {
setCurrentProject(project);
setModalVisible(true);
};
const openDeleteDialog = (id: number) => {
setDeleteDialogOpen(prev => ({...prev, [id]: true}));
};
const handleModalClose = () => {
setModalVisible(false);
setCurrentProject(undefined);
};
const closeDeleteDialog = (id: number) => {
setDeleteDialogOpen(prev => ({...prev, [id]: false}));
};
const handleSuccess = () => {
setModalVisible(false);
setCurrentProject(undefined);
loadData(form.getValues());
};
const handleEdit = (project: ProjectGroup) => {
setCurrentProject(project);
setModalVisible(true);
};
return (
<PageContainer>
{/* 页面标题 */}
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight"></h2>
<Button onClick={() => {
setCurrentProject(undefined);
setModalVisible(true);
}}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
const handleModalClose = () => {
setModalVisible(false);
setCurrentProject(undefined);
};
{/* 统计卡片 */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<FolderKanban className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.enabled}</div>
<p className="text-xs text-muted-foreground">使</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-gray-600">{stats.disabled}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</div>
const handleSuccess = () => {
setModalVisible(false);
setCurrentProject(undefined);
loadData(form.getValues());
};
const handleSearch = async (values: SearchFormValues) => {
loadData(values);
};
const columns: Column[] = [
{
accessorKey: 'projectGroupCode',
header: '项目组编码',
size: 180,
},
{
accessorKey: 'projectGroupName',
header: '项目组名称',
size: 150,
},
{
accessorKey: 'projectGroupDesc',
header: '项目组描述',
size: 200,
},
{
id: 'type',
header: '项目组类型',
size: 120,
cell: ({row}) => {
const typeInfo = getProjectTypeInfo(row.original.type);
return (
<Badge variant="outline" className="flex items-center gap-1">
{typeInfo.icon}
{typeInfo.label}
</Badge>
);
},
},
{
accessorKey: 'totalEnvironments',
header: '环境数量',
size: 100,
cell: ({row}) => (
<div className="flex items-center gap-1">
<EnvironmentOutlined/>
<span>{row.original.totalEnvironments}</span>
</div>
),
},
{
accessorKey: 'totalApplications',
header: '项目数量',
size: 100,
cell: ({row}) => (
<div className="flex items-center gap-1">
<TeamOutlined/>
<span>{row.original.totalApplications}</span>
</div>
),
},
{
accessorKey: 'enabled',
header: '状态',
size: 100,
cell: ({row}) => (
<Badge variant={row.original.enabled ? "outline" : "secondary"}>
{row.original.enabled ? '启用' : '禁用'}
</Badge>
),
},
{
accessorKey: 'sort',
header: '排序',
size: 80,
},
{
id: 'actions',
header: '操作',
size: 180,
cell: ({row}) => (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(row.original)}
>
<EditOutlined className="mr-1"/>
</Button>
<AlertDialog
open={deleteDialogOpen[row.original.id] || false}
onOpenChange={(open) => {
if (!open) closeDeleteDialog(row.original.id);
}}
>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-destructive"
onClick={() => openDeleteDialog(row.original.id)}
>
<DeleteOutlined className="mr-1"/>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => closeDeleteDialog(row.original.id)}>
</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDelete(row.original.id)}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
),
},
];
return (
<PageContainer>
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight"></h2>
<Button onClick={() => setModalVisible(true)}>
<PlusOutlined className="mr-1"/>
</Button>
</div>
<Card>
<div className="p-6">
<div className="flex items-center gap-4">
<Input
placeholder="项目组编码"
value={form.watch('projectGroupCode')}
onChange={(e) => form.setValue('projectGroupCode', e.target.value)}
className="max-w-[200px]"
/>
<Input
placeholder="项目组名称"
value={form.watch('projectGroupName')}
onChange={(e) => form.setValue('projectGroupName', e.target.value)}
className="max-w-[200px]"
/>
<Select
value={form.watch('type')}
onValueChange={(value) => form.setValue('type', value as ProjectGroupTypeEnum)}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="项目组类型"/>
</SelectTrigger>
<SelectContent>
<SelectItem value={ProjectGroupTypeEnum.PRODUCT}></SelectItem>
<SelectItem value={ProjectGroupTypeEnum.PROJECT}></SelectItem>
</SelectContent>
</Select>
<Select
value={form.watch('enabled')?.toString()}
onValueChange={(value) => form.setValue('enabled', value === 'true')}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="状态"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={() => form.reset()}>
</Button>
<Button onClick={() => loadData(form.getValues())}>
</Button>
</div>
</div>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="relative rounded-md border">
<div className="overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[140px]"></TableHead>
<TableHead className="min-w-[140px]"></TableHead>
<TableHead className="min-w-[200px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[100px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead className="min-w-[80px]"></TableHead>
<TableHead
className="sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.2)] min-w-[160px]"
>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.projectGroupCode}</TableCell>
<TableCell>{item.projectGroupName}</TableCell>
<TableCell>{item.projectGroupDesc}</TableCell>
<TableCell>
{(() => {
const typeInfo = getProjectTypeInfo(item.type);
return (
<Badge variant="outline" className="flex items-center gap-1">
{typeInfo.icon}
{typeInfo.label}
</Badge>
);
})()}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<EnvironmentOutlined/>
<span>{item.totalEnvironments}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<TeamOutlined/>
<span>{item.totalApplications}</span>
</div>
</TableCell>
<TableCell>
<Badge variant={item.enabled ? "outline" : "secondary"}>
{item.enabled ? '启用' : '禁用'}
</Badge>
</TableCell>
<TableCell>{item.sort}</TableCell>
<TableCell className="sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.2)]">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(item)}
>
<EditOutlined className="mr-1"/>
</Button>
<AlertDialog
open={deleteDialogOpen[item.id] || false}
onOpenChange={(open) => {
if (!open) closeDeleteDialog(item.id);
}}
>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-destructive"
onClick={() => openDeleteDialog(item.id)}
>
<DeleteOutlined className="mr-1"/>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => closeDeleteDialog(item.id)}>
</AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDelete(item.id)}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={pagination.pageNum}
pageSize={pagination.pageSize}
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
onPageChange={handlePageChange}
/>
</div>
</div>
</CardContent>
</Card>
<ProjectGroupModal
open={modalVisible}
onCancel={handleModalClose}
onSuccess={handleSuccess}
initialValues={currentProject}
{/* 搜索过滤 */}
<Card>
<div className="p-6">
<div className="flex items-center gap-4">
<Input
placeholder="项目组编码"
value={form.watch('projectGroupCode')}
onChange={(e) => form.setValue('projectGroupCode', e.target.value)}
className="max-w-[200px]"
/>
</PageContainer>
);
<Input
placeholder="项目组名称"
value={form.watch('projectGroupName')}
onChange={(e) => form.setValue('projectGroupName', e.target.value)}
className="max-w-[200px]"
/>
<Select
value={form.watch('type')}
onValueChange={(value) => form.setValue('type', value as ProjectGroupTypeEnum)}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="项目组类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ProjectGroupTypeEnum.PRODUCT}></SelectItem>
<SelectItem value={ProjectGroupTypeEnum.PROJECT}></SelectItem>
</SelectContent>
</Select>
<Select
value={form.watch('enabled')?.toString()}
onValueChange={(value) => form.setValue('enabled', value === 'true')}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true"></SelectItem>
<SelectItem value="false"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={() => form.reset()}>
</Button>
<Button onClick={() => loadData(form.getValues())}></Button>
</div>
</div>
</Card>
{/* 数据表格 */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[140px]"></TableHead>
<TableHead className="w-[140px]"></TableHead>
<TableHead className="w-[200px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[180px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="h-24 text-center">
</TableCell>
</TableRow>
) : (
list.map((item) => {
const typeInfo = getProjectTypeInfo(item.type);
return (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.projectGroupCode}</TableCell>
<TableCell>{item.projectGroupName}</TableCell>
<TableCell className="text-muted-foreground">
{item.projectGroupDesc}
</TableCell>
<TableCell>
<Badge variant="outline" className="inline-flex items-center gap-1">
{typeInfo.icon}
{typeInfo.label}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Layers className="h-3 w-3 text-muted-foreground" />
<span>{item.totalEnvironments}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Boxes className="h-3 w-3 text-muted-foreground" />
<span>{item.totalApplications}</span>
</div>
</TableCell>
<TableCell>
<Badge
variant={item.enabled ? 'default' : 'secondary'}
className="inline-flex"
>
{item.enabled ? '启用' : '禁用'}
</Badge>
</TableCell>
<TableCell>{item.sort}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(item)}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
setCurrentProject(item);
setDeleteDialogOpen(true);
}}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</div>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={pagination.pageNum}
pageSize={pagination.pageSize}
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
onPageChange={handlePageChange}
/>
</div>
</div>
</CardContent>
</Card>
{/* 对话框 */}
<ProjectGroupModal
open={modalVisible}
onCancel={handleModalClose}
onSuccess={handleSuccess}
initialValues={currentProject}
/>
<DeleteDialog
open={deleteDialogOpen}
record={currentProject}
onOpenChange={setDeleteDialogOpen}
onSuccess={handleSuccess}
/>
</PageContainer>
);
};
export default ProjectGroupList;