大声道撒旦

This commit is contained in:
dengqichen 2025-01-08 15:05:08 +08:00
parent 2d90609edf
commit 788ebcb72b
9 changed files with 1346 additions and 0 deletions

762
frontend/dist/scripts/generate-page.js vendored Normal file
View 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 };

View File

@ -34,6 +34,7 @@
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-progress": "^1.1.1", "@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-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",

View File

@ -80,6 +80,9 @@ importers:
'@radix-ui/react-progress': '@radix-ui/react-progress':
specifier: ^1.1.1 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) 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': '@radix-ui/react-select':
specifier: ^2.1.4 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) 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': '@types/react-dom':
optional: true 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': '@radix-ui/react-select@2.1.4':
resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==} resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==}
peerDependencies: peerDependencies:
@ -4622,6 +4638,23 @@ snapshots:
'@types/react': 18.3.18 '@types/react': 18.3.18
'@types/react-dom': 18.3.5(@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)': '@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: dependencies:
'@radix-ui/number': 1.1.0 '@radix-ui/number': 1.1.0

View 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 }

View 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 }

View 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;

View 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`);

View 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';

View File

@ -43,6 +43,7 @@ const ApplicationList = lazy(() => import('../pages/Deploy/Application/List'));
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List')); const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
const DeploymentConfigList = lazy(() => import('../pages/Deploy/Deployment/List')); const DeploymentConfigList = lazy(() => import('../pages/Deploy/Deployment/List'));
const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/List')); const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/List'));
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
const External = lazy(() => import('../pages/Deploy/External')); const External = lazy(() => import('../pages/Deploy/External'));
// 创建路由 // 创建路由
@ -94,6 +95,10 @@ const router = createBrowserRouter([
path: 'jenkins-manager', path: 'jenkins-manager',
element: <Suspense fallback={<LoadingComponent/>}><JenkinsManagerList/></Suspense> element: <Suspense fallback={<LoadingComponent/>}><JenkinsManagerList/></Suspense>
}, },
{
path: 'git-manager',
element: <Suspense fallback={<LoadingComponent/>}><GitManagerList/></Suspense>
},
{ {
path: 'external', path: 'external',
element: ( element: (