增加审批组件
This commit is contained in:
parent
d131634b49
commit
6acc7f339f
@ -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
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
104
frontend/src/pages/Deploy/External/components/DeleteDialog.tsx
vendored
Normal file
104
frontend/src/pages/Deploy/External/components/DeleteDialog.tsx
vendored
Normal 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;
|
||||
|
||||
265
frontend/src/pages/Deploy/External/components/EditDialog.tsx
vendored
Normal file
265
frontend/src/pages/Deploy/External/components/EditDialog.tsx
vendored
Normal 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;
|
||||
|
||||
662
frontend/src/pages/Deploy/External/index.tsx
vendored
662
frontend/src/pages/Deploy/External/index.tsx
vendored
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user