deploy-ease-platform/frontend/dist/scripts/generate-page.js
2025-01-08 15:05:08 +08:00

763 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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