- 支持一对多/多对一关系定义并生成到 Prisma schema - 简化流程:查询关联配置根据关系自动预填 - 修复 Handlebars 模板 HTML 转义导致的乱码问题 - 修复 controller 模板缺少 Prisma 导入的问题 - 新增页面模板 (page.hbs) 生成前端页面 - 添加 FindAllParams/PaginationQueryDto 索引签名修复类型兼容 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
421 lines
12 KiB
Handlebars
421 lines
12 KiB
Handlebars
'use client';
|
|
|
|
import type { {{pascalCase name}}Response } from '@seclusion/shared';
|
|
import { formatDate } from '@seclusion/shared';
|
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
import { MoreHorizontal, Pencil, Plus{{#if softDelete}}, RotateCcw{{/if}}, Trash2 } from 'lucide-react';
|
|
import { useCallback, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
|
|
import { {{pascalCase name}}CreateDialog } from './{{pascalCase name}}CreateDialog';
|
|
import { {{pascalCase name}}EditDialog } from './{{pascalCase name}}EditDialog';
|
|
|
|
import {
|
|
DataTable,
|
|
DataTableColumnHeader,
|
|
type PaginationState,
|
|
type SortingParams,
|
|
} from '@/components/shared/DataTable';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/components/ui/alert-dialog';
|
|
{{#if softDelete}}
|
|
import { Badge } from '@/components/ui/badge';
|
|
{{/if}}
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { PAGINATION } from '@/config/constants';
|
|
import {
|
|
use{{pascalCase pluralName}},
|
|
{{#if softDelete}}
|
|
useDeleted{{pascalCase pluralName}},
|
|
{{/if}}
|
|
useDelete{{pascalCase name}},
|
|
{{#if softDelete}}
|
|
useRestore{{pascalCase name}},
|
|
{{/if}}
|
|
} from '@/hooks/use{{pascalCase pluralName}}';
|
|
|
|
interface {{pascalCase name}}ActionsProps {
|
|
{{camelCase name}}: {{pascalCase name}}Response;
|
|
isDeleted?: boolean;
|
|
onDelete: (id: string) => void;
|
|
{{#if softDelete}}
|
|
onRestore: (id: string) => void;
|
|
{{/if}}
|
|
onEdit: ({{camelCase name}}: {{pascalCase name}}Response) => void;
|
|
}
|
|
|
|
function {{pascalCase name}}Actions({
|
|
{{camelCase name}},
|
|
isDeleted,
|
|
onDelete,
|
|
{{#if softDelete}}
|
|
onRestore,
|
|
{{/if}}
|
|
onEdit,
|
|
}: {{pascalCase name}}ActionsProps) {
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
<span className="sr-only">打开菜单</span>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>操作</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{{#if softDelete}}
|
|
{isDeleted ? (
|
|
<DropdownMenuItem onClick={() => onRestore({{camelCase name}}.id)}>
|
|
<RotateCcw className="mr-2 h-4 w-4" />
|
|
恢复
|
|
</DropdownMenuItem>
|
|
) : (
|
|
<>
|
|
<DropdownMenuItem onClick={() => onEdit({{camelCase name}})}>
|
|
<Pencil className="mr-2 h-4 w-4" />
|
|
编辑
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => onDelete({{camelCase name}}.id)}
|
|
className="text-destructive"
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
删除
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
{{else}}
|
|
<DropdownMenuItem onClick={() => onEdit({{camelCase name}})}>
|
|
<Pencil className="mr-2 h-4 w-4" />
|
|
编辑
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => onDelete({{camelCase name}}.id)}
|
|
className="text-destructive"
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
删除
|
|
</DropdownMenuItem>
|
|
{{/if}}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
export function {{pascalCase pluralName}}Table() {
|
|
// 分页状态
|
|
const [pagination, setPagination] = useState<PaginationState>({
|
|
page: PAGINATION.DEFAULT_PAGE,
|
|
pageSize: PAGINATION.DEFAULT_PAGE_SIZE,
|
|
});
|
|
|
|
// 排序状态
|
|
const [sorting, setSorting] = useState<SortingParams>({});
|
|
|
|
// 对话框状态
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [{{camelCase name}}ToDelete, set{{pascalCase name}}ToDelete] = useState<string | null>(null);
|
|
{{#if softDelete}}
|
|
const [showDeleted, setShowDeleted] = useState(false);
|
|
{{/if}}
|
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
const [{{camelCase name}}ToEdit, set{{pascalCase name}}ToEdit] = useState<{{pascalCase name}}Response | null>(
|
|
null,
|
|
);
|
|
|
|
// 查询
|
|
const {
|
|
data: {{camelCase pluralName}}Data,
|
|
isLoading: isLoading{{pascalCase pluralName}},
|
|
refetch: refetch{{pascalCase pluralName}},
|
|
} = use{{pascalCase pluralName}}({
|
|
page: pagination.page,
|
|
pageSize: pagination.pageSize,
|
|
...sorting,
|
|
});
|
|
|
|
{{#if softDelete}}
|
|
const {
|
|
data: deleted{{pascalCase pluralName}}Data,
|
|
isLoading: isLoadingDeleted,
|
|
refetch: refetchDeleted,
|
|
} = useDeleted{{pascalCase pluralName}}({
|
|
page: pagination.page,
|
|
pageSize: pagination.pageSize,
|
|
...sorting,
|
|
});
|
|
|
|
{{/if}}
|
|
// 变更
|
|
const delete{{pascalCase name}} = useDelete{{pascalCase name}}();
|
|
{{#if softDelete}}
|
|
const restore{{pascalCase name}} = useRestore{{pascalCase name}}();
|
|
{{/if}}
|
|
|
|
const handleDelete = useCallback((id: string) => {
|
|
set{{pascalCase name}}ToDelete(id);
|
|
setDeleteDialogOpen(true);
|
|
}, []);
|
|
|
|
const confirmDelete = useCallback(async () => {
|
|
if (!{{camelCase name}}ToDelete) return;
|
|
|
|
try {
|
|
await delete{{pascalCase name}}.mutateAsync({{camelCase name}}ToDelete);
|
|
toast.success('{{chineseName}}已删除');
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : '删除失败');
|
|
} finally {
|
|
setDeleteDialogOpen(false);
|
|
set{{pascalCase name}}ToDelete(null);
|
|
}
|
|
}, [{{camelCase name}}ToDelete, delete{{pascalCase name}}]);
|
|
|
|
{{#if softDelete}}
|
|
const handleRestore = useCallback(
|
|
async (id: string) => {
|
|
try {
|
|
await restore{{pascalCase name}}.mutateAsync(id);
|
|
toast.success('{{chineseName}}已恢复');
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : '恢复失败');
|
|
}
|
|
},
|
|
[restore{{pascalCase name}}],
|
|
);
|
|
|
|
{{/if}}
|
|
const handleEdit = useCallback(({{camelCase name}}: {{pascalCase name}}Response) => {
|
|
set{{pascalCase name}}ToEdit({{camelCase name}});
|
|
setEditDialogOpen(true);
|
|
}, []);
|
|
|
|
// 分页变化处理
|
|
const handlePaginationChange = useCallback((newPagination: PaginationState) => {
|
|
setPagination(newPagination);
|
|
}, []);
|
|
|
|
// 排序变化处理
|
|
const handleSortingChange = useCallback((newSorting: SortingParams) => {
|
|
setSorting(newSorting);
|
|
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
}, []);
|
|
|
|
// 列定义
|
|
const columns: ColumnDef<{{pascalCase name}}Response>[] = [
|
|
{
|
|
accessorKey: 'id',
|
|
header: 'ID',
|
|
cell: ({ row }) => (
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
{row.original.id.slice(0, 8)}...
|
|
</span>
|
|
),
|
|
},
|
|
{{#each tableColumns}}
|
|
{
|
|
accessorKey: '{{name}}',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
title="{{label}}"
|
|
sortKey={column.id}
|
|
sorting={sorting}
|
|
onSortingChange={handleSortingChange}
|
|
/>
|
|
),
|
|
{{#if (cellRenderer this)}}
|
|
cell: ({ row }) => {{{cellRenderer this}}},
|
|
{{/if}}
|
|
},
|
|
{{/each}}
|
|
{
|
|
accessorKey: 'createdAt',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
title="创建时间"
|
|
sortKey={column.id}
|
|
sorting={sorting}
|
|
onSortingChange={handleSortingChange}
|
|
/>
|
|
),
|
|
cell: ({ row }) =>
|
|
formatDate(new Date(row.original.createdAt), 'YYYY-MM-DD HH:mm'),
|
|
},
|
|
{{#if softDelete}}
|
|
...(showDeleted
|
|
? [
|
|
{
|
|
accessorKey: 'deletedAt',
|
|
header: '删除时间',
|
|
cell: ({ row }: { row: { original: {{pascalCase name}}Response } }) =>
|
|
row.original.deletedAt
|
|
? formatDate(new Date(row.original.deletedAt), 'YYYY-MM-DD HH:mm')
|
|
: '-',
|
|
} as ColumnDef<{{pascalCase name}}Response>,
|
|
]
|
|
: []),
|
|
{{/if}}
|
|
{
|
|
id: 'actions',
|
|
header: '操作',
|
|
cell: ({ row }) => (
|
|
<{{pascalCase name}}Actions
|
|
{{camelCase name}}={row.original}
|
|
{{#if softDelete}}
|
|
isDeleted={showDeleted}
|
|
{{/if}}
|
|
onDelete={handleDelete}
|
|
{{#if softDelete}}
|
|
onRestore={handleRestore}
|
|
{{/if}}
|
|
onEdit={handleEdit}
|
|
/>
|
|
),
|
|
},
|
|
];
|
|
|
|
{{#if softDelete}}
|
|
const data = showDeleted ? deleted{{pascalCase pluralName}}Data : {{camelCase pluralName}}Data;
|
|
const isLoading = showDeleted ? isLoadingDeleted : isLoading{{pascalCase pluralName}};
|
|
{{else}}
|
|
const data = {{camelCase pluralName}}Data;
|
|
const isLoading = isLoading{{pascalCase pluralName}};
|
|
{{/if}}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 工具栏 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{{#if softDelete}}
|
|
<Button
|
|
variant={!showDeleted ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => {
|
|
setShowDeleted(false);
|
|
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
}}
|
|
>
|
|
{{chineseName}}列表
|
|
{ {{camelCase pluralName}}Data && (
|
|
<Badge variant="secondary" className="ml-2">
|
|
{ {{camelCase pluralName}}Data.total}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant={showDeleted ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => {
|
|
setShowDeleted(true);
|
|
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
}}
|
|
>
|
|
已删除
|
|
{deleted{{pascalCase pluralName}}Data && (
|
|
<Badge variant="secondary" className="ml-2">
|
|
{deleted{{pascalCase pluralName}}Data.total}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
{{/if}}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() =>
|
|
{{#if softDelete}}
|
|
showDeleted ? refetchDeleted() : refetch{{pascalCase pluralName}}()
|
|
{{else}}
|
|
refetch{{pascalCase pluralName}}()
|
|
{{/if}}
|
|
}
|
|
>
|
|
刷新
|
|
</Button>
|
|
<Button size="sm" onClick={() => setCreateDialogOpen(true)}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
新建{{chineseName}}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 数据表格 */}
|
|
<DataTable
|
|
columns={columns}
|
|
data={data?.items ?? []}
|
|
pagination={pagination}
|
|
paginationInfo={
|
|
data ? { total: data.total, totalPages: data.totalPages } : undefined
|
|
}
|
|
onPaginationChange={handlePaginationChange}
|
|
manualPagination
|
|
sorting={sorting}
|
|
onSortingChange={handleSortingChange}
|
|
manualSorting
|
|
isLoading={isLoading}
|
|
emptyMessage={
|
|
{{#if softDelete}}
|
|
showDeleted ? '暂无已删除{{chineseName}}' : '暂无{{chineseName}}'
|
|
{{else}}
|
|
'暂无{{chineseName}}'
|
|
{{/if}}
|
|
}
|
|
/>
|
|
|
|
{/* 删除确认弹窗 */}
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
确定要删除该{{chineseName}}吗?{{#if softDelete}}删除后可在「已删除」列表中恢复。{{/if}}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={confirmDelete}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
删除
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* 创建弹窗 */}
|
|
<{{pascalCase name}}CreateDialog
|
|
open={createDialogOpen}
|
|
onOpenChange={setCreateDialogOpen}
|
|
/>
|
|
|
|
{/* 编辑弹窗 */}
|
|
<{{pascalCase name}}EditDialog
|
|
{{camelCase name}}={ {{camelCase name}}ToEdit}
|
|
open={editDialogOpen}
|
|
onOpenChange={setEditDialogOpen}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|