大声道撒旦
This commit is contained in:
parent
2d90609edf
commit
788ebcb72b
762
frontend/dist/scripts/generate-page.js
vendored
Normal file
762
frontend/dist/scripts/generate-page.js
vendored
Normal file
@ -0,0 +1,762 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { createInterface } from 'readline';
|
||||
// 获取当前文件的目录
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
// 生成 types.ts
|
||||
function generateTypes(options) {
|
||||
const { name } = options;
|
||||
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return `import type { BaseResponse, BaseQuery } from '@/types/base';
|
||||
|
||||
export interface ${typeName}Response extends BaseResponse {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
// TODO: 添加其他字段
|
||||
}
|
||||
|
||||
export interface ${typeName}Query extends BaseQuery {
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
// TODO: 添加其他查询字段
|
||||
}
|
||||
|
||||
export interface ${typeName}Request {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
sort: number;
|
||||
// TODO: 添加其他请求字段
|
||||
}
|
||||
`;
|
||||
}
|
||||
// 生成 schema.ts
|
||||
function generateSchema(options) {
|
||||
const { name } = options;
|
||||
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return `import * as z from "zod";
|
||||
|
||||
export const searchFormSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const ${typeName.toLowerCase()}FormSchema = z.object({
|
||||
name: z.string().min(1, "请输入名称").max(50, "名称不能超过50个字符"),
|
||||
description: z.string().max(200, "描述不能超过200个字符").optional(),
|
||||
enabled: z.boolean().default(true),
|
||||
sort: z.number().min(0).default(0),
|
||||
});
|
||||
|
||||
export type SearchFormValues = z.infer<typeof searchFormSchema>;
|
||||
export type ${typeName}FormValues = z.infer<typeof ${typeName.toLowerCase()}FormSchema>;
|
||||
`;
|
||||
}
|
||||
// 生成 service.ts
|
||||
function generateService(options) {
|
||||
const { name, baseUrl } = options;
|
||||
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return `import request from '@/utils/request';
|
||||
import type { Page } from '@/types/base';
|
||||
import type { ${typeName}Response, ${typeName}Query, ${typeName}Request } from './types';
|
||||
|
||||
const BASE_URL = '${baseUrl}';
|
||||
|
||||
// 创建
|
||||
export const create${typeName} = (data: ${typeName}Request) =>
|
||||
request.post<void>(BASE_URL, data);
|
||||
|
||||
// 更新
|
||||
export const update${typeName} = (id: number, data: ${typeName}Request) =>
|
||||
request.put<void>(\`\${BASE_URL}/\${id}\`, data);
|
||||
|
||||
// 删除
|
||||
export const delete${typeName} = (id: number) =>
|
||||
request.delete<void>(\`\${BASE_URL}/\${id}\`);
|
||||
|
||||
// 获取详情
|
||||
export const get${typeName} = (id: number) =>
|
||||
request.get<${typeName}Response>(\`\${BASE_URL}/\${id}\`);
|
||||
|
||||
// 分页查询列表
|
||||
export const get${typeName}Page = (params?: ${typeName}Query) =>
|
||||
request.get<Page<${typeName}Response>>(\`\${BASE_URL}/page\`, { params });
|
||||
|
||||
// 获取所有列表
|
||||
export const get${typeName}List = () =>
|
||||
request.get<${typeName}Response[]>(BASE_URL);
|
||||
|
||||
// 条件查询列表
|
||||
export const get${typeName}ListByCondition = (params?: ${typeName}Query) =>
|
||||
request.get<${typeName}Response[]>(\`\${BASE_URL}/list\`, { params });
|
||||
`;
|
||||
}
|
||||
// 生成 Modal 组件
|
||||
function generateModal(options) {
|
||||
const { name } = options;
|
||||
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return `import React, { useEffect } from 'react';
|
||||
import type { ${typeName}Response } from '../types';
|
||||
import { create${typeName}, update${typeName} } from '../service';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ${typeName.toLowerCase()}FormSchema, type ${typeName}FormValues } from '../schema';
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
interface ${typeName}ModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
initialValues?: ${typeName}Response;
|
||||
}
|
||||
|
||||
const ${typeName}Modal: React.FC<${typeName}ModalProps> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
initialValues,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const isEdit = !!initialValues?.id;
|
||||
|
||||
const form = useForm<${typeName}FormValues>({
|
||||
resolver: zodResolver(${typeName.toLowerCase()}FormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
enabled: true,
|
||||
sort: 0,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
form.reset({
|
||||
name: initialValues.name,
|
||||
description: initialValues.description || "",
|
||||
enabled: initialValues.enabled,
|
||||
sort: initialValues.sort,
|
||||
});
|
||||
}
|
||||
}, [initialValues, form]);
|
||||
|
||||
const handleSubmit = async (values: ${typeName}FormValues) => {
|
||||
try {
|
||||
if (isEdit) {
|
||||
await update${typeName}(initialValues.id, values);
|
||||
} else {
|
||||
await create${typeName}(values);
|
||||
}
|
||||
toast({
|
||||
title: \`\${isEdit ? '更新' : '创建'}成功\`,
|
||||
duration: 3000,
|
||||
});
|
||||
form.reset();
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: \`\${isEdit ? '更新' : '创建'}失败\`,
|
||||
description: error instanceof Error ? error.message : undefined,
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="请输入名称"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="请输入描述"
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({field}) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>状态</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sort"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>排序</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
确定
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ${typeName}Modal;
|
||||
`;
|
||||
}
|
||||
// 生成列表页面组件
|
||||
function generateListPage(options) {
|
||||
const { name, description } = options;
|
||||
const typeName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return `import React, { useState, useEffect } from 'react';
|
||||
import { PageContainer } from '@/components/ui/page-container';
|
||||
import { Plus, Pencil, Trash2, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
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";
|
||||
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";
|
||||
import type { ${typeName}Response, ${typeName}Query } from './types';
|
||||
import { get${typeName}Page, delete${typeName} } from './service';
|
||||
import ${typeName}Modal from './components/${typeName}Modal';
|
||||
|
||||
interface Column {
|
||||
accessorKey?: keyof ${typeName}Response;
|
||||
id?: string;
|
||||
header: string;
|
||||
size: number;
|
||||
cell?: (props: { row: { original: ${typeName}Response } }) => React.ReactNode;
|
||||
}
|
||||
|
||||
const ${typeName}List: React.FC = () => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRecord, setCurrentRecord] = useState<${typeName}Response>();
|
||||
const [list, setList] = useState<${typeName}Response[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
totalElements: 0,
|
||||
});
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<SearchFormValues>({
|
||||
resolver: zodResolver(searchFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
enabled: undefined,
|
||||
}
|
||||
});
|
||||
|
||||
const loadData = async (params?: ${typeName}Query) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const queryParams: ${typeName}Query = {
|
||||
...params,
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
};
|
||||
const data = await get${typeName}Page(queryParams);
|
||||
setList(data.content || []);
|
||||
setPagination({
|
||||
...pagination,
|
||||
totalElements: data.totalElements,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "获取列表失败",
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageNum: page,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData(form.getValues());
|
||||
}, [pagination.pageNum, pagination.pageSize]);
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await delete${typeName}(id);
|
||||
toast({
|
||||
title: "删除成功",
|
||||
duration: 3000,
|
||||
});
|
||||
loadData(form.getValues());
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "删除失败",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setCurrentRecord(undefined);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: ${typeName}Response) => {
|
||||
setCurrentRecord(record);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModalVisible(false);
|
||||
setCurrentRecord(undefined);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setModalVisible(false);
|
||||
setCurrentRecord(undefined);
|
||||
loadData(form.getValues());
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.reset({
|
||||
name: "",
|
||||
enabled: undefined,
|
||||
});
|
||||
loadData();
|
||||
};
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: '名称',
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: '描述',
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'enabled',
|
||||
header: '状态',
|
||||
size: 100,
|
||||
cell: ({row}) => (
|
||||
<Badge variant={row.original.enabled ? "outline" : "secondary"}>
|
||||
{row.original.enabled ? '启用' : '禁用'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'sort',
|
||||
header: '排序',
|
||||
size: 80,
|
||||
},
|
||||
{
|
||||
accessorKey: 'createTime',
|
||||
header: '创建时间',
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '操作',
|
||||
size: 180,
|
||||
cell: ({row}) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
编辑
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
删除
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确定要删除吗?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
删除后将无法恢复,请谨慎操作
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</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">${description}</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button onClick={handleAdd}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
placeholder="名称"
|
||||
{...form.register('name')}
|
||||
className="max-w-[200px]"
|
||||
/>
|
||||
<Select
|
||||
value={form.getValues('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={handleReset}>
|
||||
重置
|
||||
</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>
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={column.accessorKey || column.id}
|
||||
style={{width: column.size}}
|
||||
>
|
||||
{column.header}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
<div className="flex justify-center items-center">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : list.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
list.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
{columns.map((column) => (
|
||||
<TableCell
|
||||
key={column.accessorKey || column.id}
|
||||
>
|
||||
{column.cell
|
||||
? column.cell({row: {original: item}})
|
||||
: item[column.accessorKey!]}
|
||||
</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>
|
||||
|
||||
{modalVisible && (
|
||||
<${typeName}Modal
|
||||
open={modalVisible}
|
||||
onCancel={handleModalClose}
|
||||
onSuccess={handleSuccess}
|
||||
initialValues={currentRecord}
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ${typeName}List;
|
||||
`;
|
||||
}
|
||||
// 生成文件
|
||||
async function generateFiles(options) {
|
||||
const { name, pageType = 'list' } = options;
|
||||
const basePath = join(process.cwd(), 'src/pages', name, 'List');
|
||||
const componentsPath = join(basePath, 'components');
|
||||
// 创建目录
|
||||
await fs.mkdir(basePath, { recursive: true });
|
||||
await fs.mkdir(componentsPath, { recursive: true });
|
||||
// 生成文件
|
||||
switch (pageType) {
|
||||
case 'list':
|
||||
await fs.writeFile(join(basePath, 'types.ts'), generateTypes(options));
|
||||
await fs.writeFile(join(basePath, 'schema.ts'), generateSchema(options));
|
||||
await fs.writeFile(join(basePath, 'service.ts'), generateService(options));
|
||||
await fs.writeFile(join(basePath, 'index.tsx'), generateListPage(options));
|
||||
await fs.writeFile(join(componentsPath, `${options.name.charAt(0).toUpperCase() + options.name.slice(1)}Modal.tsx`), generateModal(options));
|
||||
break;
|
||||
case 'detail':
|
||||
case 'form':
|
||||
// 未来实现
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 主函数
|
||||
async function generate(options) {
|
||||
try {
|
||||
await generateFiles(options);
|
||||
console.log('✨ 页面生成成功!');
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 页面生成失败:', error);
|
||||
}
|
||||
}
|
||||
// 创建命令行交互界面
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
// 提示用户输入
|
||||
const prompt = (question) => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
// 交互式生成页面
|
||||
async function generateInteractive() {
|
||||
try {
|
||||
console.log('👋 欢迎使用页面生成器!');
|
||||
console.log('-------------------');
|
||||
// 获取模块名称
|
||||
const name = await prompt('请输入模块名称(如:user): ');
|
||||
if (!name) {
|
||||
throw new Error('模块名称不能为空!');
|
||||
}
|
||||
// 获取模块描述
|
||||
const description = await prompt('请输入模块描述(如:用户管理): ');
|
||||
if (!description) {
|
||||
throw new Error('模块描述不能为空!');
|
||||
}
|
||||
// 获取API路径
|
||||
const defaultBaseUrl = `/api/v1/${name.toLowerCase()}`;
|
||||
const baseUrl = await prompt(`请输入API路径 (默认: ${defaultBaseUrl}): `) || defaultBaseUrl;
|
||||
// 获取页面类型(预留)
|
||||
const pageType = 'list'; // 目前固定为list
|
||||
// 生成页面
|
||||
generate({
|
||||
name,
|
||||
description,
|
||||
baseUrl,
|
||||
pageType,
|
||||
});
|
||||
console.log('');
|
||||
console.log('✨ 页面生成成功!');
|
||||
console.log('📁 生成的文件位置:');
|
||||
console.log(`src/pages/${name}/list/`);
|
||||
console.log('');
|
||||
console.log('🎉 包含以下文件:');
|
||||
console.log('- types.ts');
|
||||
console.log('- schema.ts');
|
||||
console.log('- service.ts');
|
||||
console.log('- index.tsx');
|
||||
console.log(`- components/${name.charAt(0).toUpperCase() + name.slice(1)}Modal.tsx`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('❌ 页面生成失败:', error);
|
||||
}
|
||||
finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
// 根据命令行参数判断是否使用交互模式
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 0) {
|
||||
// 无参数时使用交互模式
|
||||
generateInteractive();
|
||||
}
|
||||
else {
|
||||
// 有参数时使用命令行模式
|
||||
generate({
|
||||
name: args[0],
|
||||
description: args[1] || '管理',
|
||||
baseUrl: args[2] || `/api/v1/${args[0]}`,
|
||||
pageType: 'list',
|
||||
});
|
||||
}
|
||||
}
|
||||
export { generate };
|
||||
@ -34,6 +34,7 @@
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
|
||||
@ -80,6 +80,9 @@ importers:
|
||||
'@radix-ui/react-progress':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-scroll-area':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-select':
|
||||
specifier: ^2.1.4
|
||||
version: 2.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -1104,6 +1107,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-scroll-area@1.2.2':
|
||||
resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-select@2.1.4':
|
||||
resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==}
|
||||
peerDependencies:
|
||||
@ -4622,6 +4638,23 @@ snapshots:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-scroll-area@1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.0
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-select@2.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.0
|
||||
|
||||
46
frontend/src/components/ui/scroll-area.tsx
Normal file
46
frontend/src/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
15
frontend/src/components/ui/skeleton.tsx
Normal file
15
frontend/src/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
363
frontend/src/pages/Deploy/GitManager/List/index.tsx
Normal file
363
frontend/src/pages/Deploy/GitManager/List/index.tsx
Normal file
@ -0,0 +1,363 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
GitBranch,
|
||||
GitFork,
|
||||
RefreshCw,
|
||||
Link2,
|
||||
FolderGit2
|
||||
} from "lucide-react";
|
||||
import { message } from 'antd';
|
||||
import type { GitInstance, GitInstanceInfo, RepositoryGroup, RepositoryProject } from './types';
|
||||
import {
|
||||
getGitInstances,
|
||||
getGitInstanceInfo,
|
||||
syncAllGitData,
|
||||
syncGitGroups,
|
||||
syncGitProjects,
|
||||
syncGitBranches
|
||||
} from './service';
|
||||
|
||||
const GitManager: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [instances, setInstances] = useState<GitInstance[]>([]);
|
||||
const [selectedInstance, setSelectedInstance] = useState<number>();
|
||||
const [instanceInfo, setInstanceInfo] = useState<GitInstanceInfo>();
|
||||
|
||||
// 加载Git实例列表
|
||||
const loadInstances = async () => {
|
||||
try {
|
||||
const response = await getGitInstances();
|
||||
if (response && Array.isArray(response)) {
|
||||
setInstances(response);
|
||||
if (response.length > 0) {
|
||||
setSelectedInstance(response[0].id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载Git实例失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载实例信息
|
||||
const loadInstanceInfo = async () => {
|
||||
if (!selectedInstance) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getGitInstanceInfo(selectedInstance);
|
||||
setInstanceInfo(data);
|
||||
} catch (error) {
|
||||
message.error('加载实例信息失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 同步所有数据
|
||||
const handleSyncAll = async () => {
|
||||
if (!selectedInstance) return;
|
||||
try {
|
||||
await syncAllGitData(selectedInstance);
|
||||
message.success('同步任务已启动');
|
||||
loadInstanceInfo();
|
||||
} catch (error) {
|
||||
message.error('同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 同步仓库组
|
||||
const handleSyncGroups = async () => {
|
||||
if (!selectedInstance) return;
|
||||
try {
|
||||
await syncGitGroups(selectedInstance);
|
||||
message.success('仓库组同步任务已启动');
|
||||
loadInstanceInfo();
|
||||
} catch (error) {
|
||||
message.error('同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 同步项目
|
||||
const handleSyncProjects = async (groupId: number) => {
|
||||
if (!selectedInstance) return;
|
||||
try {
|
||||
await syncGitProjects(selectedInstance, groupId);
|
||||
message.success('项目同步任务已启动');
|
||||
loadInstanceInfo();
|
||||
} catch (error) {
|
||||
message.error('同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 同步分支
|
||||
const handleSyncBranches = async (projectId: number) => {
|
||||
if (!selectedInstance) return;
|
||||
try {
|
||||
await syncGitBranches(selectedInstance, projectId);
|
||||
message.success('分支同步任务已启动');
|
||||
loadInstanceInfo();
|
||||
} catch (error) {
|
||||
message.error('同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量同步项目
|
||||
const handleSyncAllProjects = async () => {
|
||||
if (!selectedInstance || !instanceInfo?.repositoryGroupList) return;
|
||||
try {
|
||||
await Promise.all(
|
||||
instanceInfo.repositoryGroupList.map(group =>
|
||||
syncGitProjects(selectedInstance, group.groupId)
|
||||
)
|
||||
);
|
||||
message.success('所有项目同步任务已启动');
|
||||
loadInstanceInfo();
|
||||
} catch (error) {
|
||||
message.error('同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量同步分支
|
||||
const handleSyncAllBranches = async () => {
|
||||
if (!selectedInstance || !instanceInfo?.repositoryProjectList) return;
|
||||
try {
|
||||
await Promise.all(
|
||||
instanceInfo.repositoryProjectList.map(project =>
|
||||
syncGitBranches(selectedInstance, project.projectId)
|
||||
)
|
||||
);
|
||||
message.success('所有分支同步任务已启动');
|
||||
loadInstanceInfo();
|
||||
} catch (error) {
|
||||
message.error('同步失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadInstances();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedInstance) {
|
||||
loadInstanceInfo();
|
||||
}
|
||||
}, [selectedInstance]);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
header={{
|
||||
title: 'Git 仓库管理',
|
||||
extra: [
|
||||
<Button key="sync" onClick={handleSyncAll}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
同步所有数据
|
||||
</Button>,
|
||||
<Select
|
||||
key="select"
|
||||
value={selectedInstance?.toString()}
|
||||
onValueChange={(value) => setSelectedInstance(Number(value))}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="选择Git实例" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{instances.map(instance => (
|
||||
<SelectItem key={instance.id} value={instance.id.toString()}>
|
||||
{instance.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
],
|
||||
}}
|
||||
>
|
||||
{selectedInstance && instances.find(i => i.id === selectedInstance) && (
|
||||
<Card className="mb-6">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle>{instances.find(i => i.id === selectedInstance)?.name}</CardTitle>
|
||||
<CardDescription className="text-sm text-muted-foreground">
|
||||
{instances.find(i => i.id === selectedInstance)?.url}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<Card className="col-span-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
<div className="flex items-center">
|
||||
<FolderGit2 className="mr-2 h-5 w-5" />
|
||||
{instanceInfo?.totalGroups || 0}
|
||||
</div>
|
||||
<div className="text-sm font-normal text-muted-foreground mt-1">仓库组</div>
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm" onClick={handleSyncGroups}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Last sync: {instanceInfo?.lastSyncGroupsTime ? new Date(instanceInfo.lastSyncGroupsTime).toLocaleString() : 'Never'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
<div className="flex items-center">
|
||||
<GitFork className="mr-2 h-5 w-5" />
|
||||
{instanceInfo?.totalProjects || 0}
|
||||
</div>
|
||||
<div className="text-sm font-normal text-muted-foreground mt-1">项目</div>
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm" onClick={handleSyncAllProjects}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Last sync: {instanceInfo?.lastSyncProjectsTime ? new Date(instanceInfo.lastSyncProjectsTime).toLocaleString() : 'Never'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="mr-2 h-5 w-5" />
|
||||
{instanceInfo?.totalBranches || 0}
|
||||
</div>
|
||||
<div className="text-sm font-normal text-muted-foreground mt-1">分支</div>
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm" onClick={handleSyncAllBranches}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Last sync: {instanceInfo?.lastSyncBranchesTime ? new Date(instanceInfo.lastSyncBranchesTime).toLocaleString() : 'Never'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="groups" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="groups">仓库组</TabsTrigger>
|
||||
<TabsTrigger value="projects">项目</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="groups">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">名称</TableHead>
|
||||
<TableHead className="w-[200px]">路径</TableHead>
|
||||
<TableHead className="w-[100px]">可见性</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead className="w-[150px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{instanceInfo?.repositoryGroupList?.map((group) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell className="font-medium">{group.name}</TableCell>
|
||||
<TableCell>{group.path}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={group.visibility === 'private' ? 'secondary' : 'default'}>
|
||||
{group.visibility}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{group.description}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleSyncProjects(group.groupId)}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
同步项目
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="projects">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">名称</TableHead>
|
||||
<TableHead className="w-[200px]">路径</TableHead>
|
||||
<TableHead className="w-[120px]">默认分支</TableHead>
|
||||
<TableHead>最后活动时间</TableHead>
|
||||
<TableHead className="w-[200px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{instanceInfo?.repositoryProjectList?.map((project) => (
|
||||
<TableRow key={project.id}>
|
||||
<TableCell className="font-medium">{project.name}</TableCell>
|
||||
<TableCell>{project.path}</TableCell>
|
||||
<TableCell>{project.isDefaultBranch}</TableCell>
|
||||
<TableCell>{new Date(project.lastActivityAt).toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleSyncBranches(project.projectId)}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
同步分支
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<a href={project.webUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Link2 className="mr-2 h-4 w-4" />
|
||||
查看
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitManager;
|
||||
33
frontend/src/pages/Deploy/GitManager/List/service.ts
Normal file
33
frontend/src/pages/Deploy/GitManager/List/service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request';
|
||||
import type { GitInstanceInfo } from './types';
|
||||
import { getExternalSystems } from '@/pages/Deploy/External/service';
|
||||
import { SystemType } from '@/pages/Deploy/External/types';
|
||||
|
||||
const BASE_URL = '/api/v1/repository-manager';
|
||||
|
||||
// 获取 Git 实例列表
|
||||
export const getGitInstances = () =>
|
||||
getExternalSystems({
|
||||
type: SystemType.GIT,
|
||||
enabled: true
|
||||
}).then(response => response.content);
|
||||
|
||||
// 获取 Git 实例信息
|
||||
export const getGitInstanceInfo = (externalSystemId: number) =>
|
||||
request.get<GitInstanceInfo>(`${BASE_URL}/${externalSystemId}/instance`);
|
||||
|
||||
// 同步所有 Git 数据
|
||||
export const syncAllGitData = (externalSystemId: number) =>
|
||||
request.post<void>(`${BASE_URL}/${externalSystemId}/sync-all`);
|
||||
|
||||
// 同步 Git 仓库组
|
||||
export const syncGitGroups = (externalSystemId: number) =>
|
||||
request.post<void>(`${BASE_URL}/${externalSystemId}/sync-groups`);
|
||||
|
||||
// 同步 Git 项目
|
||||
export const syncGitProjects = (externalSystemId: number, groupId: number) =>
|
||||
request.post<void>(`${BASE_URL}/${externalSystemId}/groups/${groupId}/sync-projects`);
|
||||
|
||||
// 同步 Git 分支
|
||||
export const syncGitBranches = (externalSystemId: number, projectId: number) =>
|
||||
request.post<void>(`${BASE_URL}/${externalSystemId}/projects/${projectId}/sync-branches`);
|
||||
88
frontend/src/pages/Deploy/GitManager/List/types.ts
Normal file
88
frontend/src/pages/Deploy/GitManager/List/types.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { BaseResponse } from '@/types/base';
|
||||
import type { ExternalSystemResponse } from '@/pages/Deploy/External/types';
|
||||
|
||||
// Git仓库组
|
||||
export interface RepositoryGroup {
|
||||
id: number;
|
||||
groupId: number;
|
||||
name: string;
|
||||
path: string;
|
||||
description: string;
|
||||
visibility: string;
|
||||
parentId: number | null;
|
||||
webUrl: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
// Git项目
|
||||
export interface RepositoryProject {
|
||||
id: number;
|
||||
projectId: number;
|
||||
name: string;
|
||||
path: string;
|
||||
description: string;
|
||||
visibility: string;
|
||||
groupId: number;
|
||||
isDefaultBranch: string;
|
||||
webUrl: string;
|
||||
sshUrl: string;
|
||||
httpUrl: string;
|
||||
lastActivityAt: string;
|
||||
}
|
||||
|
||||
// Git实例信息
|
||||
export interface GitInstanceInfo {
|
||||
totalGroups: number;
|
||||
lastSyncGroupsTime: string;
|
||||
totalProjects: number;
|
||||
lastSyncProjectsTime: string;
|
||||
totalBranches: number;
|
||||
lastSyncBranchesTime: string;
|
||||
repositoryGroupList: RepositoryGroup[];
|
||||
repositoryProjectList: RepositoryProject[];
|
||||
}
|
||||
|
||||
// 使用外部系统响应作为 Git 实例
|
||||
export type GitInstance = ExternalSystemResponse;
|
||||
|
||||
// Git 仓库详情
|
||||
export interface GitRepositoryDTO extends BaseResponse {
|
||||
repoName: string;
|
||||
repoUrl: string;
|
||||
description: string;
|
||||
defaultBranch: string;
|
||||
visibility: 'public' | 'private';
|
||||
lastCommitTime?: string;
|
||||
lastCommitMessage?: string;
|
||||
lastCommitAuthor?: string;
|
||||
totalBranches: number;
|
||||
totalTags: number;
|
||||
totalCommits: number;
|
||||
externalSystemId: number;
|
||||
}
|
||||
|
||||
// Git 分支信息
|
||||
export interface GitBranchDTO extends BaseResponse {
|
||||
branchName: string;
|
||||
isDefault: boolean;
|
||||
isProtected: boolean;
|
||||
lastCommitSha: string;
|
||||
lastCommitMessage: string;
|
||||
lastCommitAuthor: string;
|
||||
lastCommitTime: string;
|
||||
repositoryId: number;
|
||||
}
|
||||
|
||||
// Git 标签信息
|
||||
export interface GitTagDTO extends BaseResponse {
|
||||
tagName: string;
|
||||
tagMessage: string;
|
||||
taggerName: string;
|
||||
taggerEmail: string;
|
||||
taggedTime: string;
|
||||
commitSha: string;
|
||||
repositoryId: number;
|
||||
}
|
||||
|
||||
// 同步类型
|
||||
export type SyncType = 'repositories' | 'branches' | 'tags';
|
||||
@ -43,6 +43,7 @@ const ApplicationList = lazy(() => import('../pages/Deploy/Application/List'));
|
||||
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
|
||||
const DeploymentConfigList = lazy(() => import('../pages/Deploy/Deployment/List'));
|
||||
const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/List'));
|
||||
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
|
||||
const External = lazy(() => import('../pages/Deploy/External'));
|
||||
|
||||
// 创建路由
|
||||
@ -94,6 +95,10 @@ const router = createBrowserRouter([
|
||||
path: 'jenkins-manager',
|
||||
element: <Suspense fallback={<LoadingComponent/>}><JenkinsManagerList/></Suspense>
|
||||
},
|
||||
{
|
||||
path: 'git-manager',
|
||||
element: <Suspense fallback={<LoadingComponent/>}><GitManagerList/></Suspense>
|
||||
},
|
||||
{
|
||||
path: 'external',
|
||||
element: (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user