Files
SecMPS/gb28181_web-main/app/components/xui/edit-sheet.tsx
T

375 lines
9.4 KiB
TypeScript

import { useMutation } from "@tanstack/react-query";
import type { FormInstance } from "antd";
import { Button, Form, Modal } from "antd";
import { SquarePlus } from "lucide-react";
import React, {
Children,
isValidElement,
useImperativeHandle,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Button as ShadcnButton } from "~/components/ui/button";
import { ErrorHandle } from "~/service/config/error";
export interface PFormProps {
onAddSuccess?: () => void; // 添加成功回调
onEditSuccess?: (data: any) => void; // 编辑成功回调
ref: React.RefObject<EditSheetImpl | null>; // 控制反转
}
// 步骤配置
export interface StepConfig {
title: string; // 步骤标题
fields: string[]; // 该步骤包含的字段名
}
interface EditSheetProps {
title: string; // 标题
description?: string; // 描述
children: React.ReactNode; // 表单内容
trigger?: React.ReactNode; // 触发器按钮
mutation: {
// api 请求
add: (values: any) => Promise<any>;
edit: (id: string, values: any) => Promise<any>;
};
onSuccess?: {
// 成功回调
add?: () => void;
edit?: (data: any) => void;
};
ref?: React.Ref<EditSheetImpl>;
form: FormInstance; // Ant Design Form 实例
width?: number | string; // Modal 宽度,默认 520
steps?: StepConfig[]; // 步骤配置,如果不提供则自动分组
fieldsPerStep?: number; // 每步字段数,默认 2
}
export interface EditSheetImpl {
edit: (values: any) => void; // 编辑时传入表单的值,打开弹窗
}
/**
* 统一的多步骤表单编辑弹窗组件
* 支持新增和编辑两种模式,通过 form 中是否有 id 字段区分
* 自动将表单字段按每 3 个分组为一个步骤
*/
export function EditSheet({
title,
description,
children,
trigger,
mutation,
onSuccess,
ref,
form,
width = 520,
steps: customSteps,
fieldsPerStep = 2,
}: EditSheetProps) {
const { t } = useTranslation("common");
const [open, setOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
// 解析 children 中的 Form.Item,提取字段信息
const parseFormItems = () => {
const items: { name: string; element: React.ReactNode; hidden: boolean }[] =
[];
const traverse = (node: React.ReactNode) => {
Children.forEach(node, (child) => {
if (isValidElement(child)) {
// 检查是否是 Form.Item
if (
child.type === Form.Item ||
(child.type as any)?.displayName === "FormItem"
) {
const props = child.props as any;
const name = props.name;
const hidden = props.hidden === true;
if (name) {
items.push({ name, element: child, hidden });
}
}
// 递归处理子元素
if ((child.props as { children?: React.ReactNode })?.children) {
traverse((child.props as { children?: React.ReactNode }).children);
}
}
});
};
traverse(children);
return items;
};
const formItems = parseFormItems();
// 过滤出可见的表单项(用于分步)
const visibleItems = formItems.filter((item) => !item.hidden);
// 隐藏的表单项(始终渲染)
const hiddenItems = formItems.filter((item) => item.hidden);
// 自动生成步骤配置(每步 fieldsPerStep 个字段,最多 3 步)
const generateSteps = (): StepConfig[] => {
if (customSteps) return customSteps;
const stepsConfig: StepConfig[] = [];
const totalItems = visibleItems.length;
// 按 fieldsPerStep 分组,最多 3 步
for (
let i = 0;
i < totalItems && stepsConfig.length < 3;
i += fieldsPerStep
) {
const stepItems = visibleItems.slice(i, i + fieldsPerStep);
stepsConfig.push({
title: `${t("step")} ${stepsConfig.length + 1}`,
fields: stepItems.map((item) => item.name),
});
}
return stepsConfig;
};
const stepsConfig = generateSteps();
const totalSteps = stepsConfig.length;
const isMultiStep = totalSteps > 1;
// 获取当前步骤应该显示的字段
const getCurrentStepFields = (): string[] => {
if (!isMultiStep) return visibleItems.map((item) => item.name);
return stepsConfig[currentStep]?.fields || [];
};
// 判断当前是编辑模式还是新增模式
const isEditMode = () => {
const values = form.getFieldsValue();
return !!values.id;
};
useImperativeHandle(ref, () => ({
edit(values: any) {
console.log("🚀 ~ edit ~ values:", values);
form.setFieldsValue(values);
setCurrentStep(0);
setOpen(true);
},
}));
const { mutateAsync, isPending } = useMutation({
mutationFn: async (values: any) => {
if (values.id) {
return await mutation.edit(values.id, values);
}
return await mutation.add(values);
},
onSuccess(data, variables) {
if (variables.id) {
onSuccess?.edit?.(data.data);
} else {
onSuccess?.add?.();
}
handleClose();
},
onError: ErrorHandle,
});
// 验证当前步骤的字段
const validateCurrentStep = async (): Promise<boolean> => {
const currentFields = getCurrentStepFields();
try {
await form.validateFields(currentFields);
return true;
} catch {
return false;
}
};
// 下一步
const handleNext = async () => {
const isValid = await validateCurrentStep();
if (isValid && currentStep < totalSteps - 1) {
setCurrentStep(currentStep + 1);
}
};
// 上一步
const handlePrev = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSubmit = async () => {
try {
// 最后一步时验证所有字段
const values = await form.validateFields();
await mutateAsync(values);
} catch (error) {
console.log("表单验证失败:", error);
}
};
const handleClose = () => {
setOpen(false);
setCurrentStep(0);
// 延迟重置表单,避免关闭动画时内容闪烁
setTimeout(() => {
form.resetFields();
}, 200);
};
const handleCancel = () => {
handleClose();
};
// 打开弹窗(用于新增模式)
const handleOpen = () => {
form.resetFields();
setCurrentStep(0);
setOpen(true);
};
// 渲染触发按钮
const renderTrigger = () => {
if (trigger === null) {
return null;
}
const defaultTrigger = (
<ShadcnButton onClick={handleOpen}>
<SquarePlus className="mr-2 h-4 w-4" />
{t("add")}
</ShadcnButton>
);
if (trigger) {
if (React.isValidElement(trigger)) {
return React.cloneElement(trigger as React.ReactElement<any>, {
onClick: (e: React.MouseEvent) => {
const originalOnClick = (trigger as React.ReactElement<any>).props
?.onClick;
if (originalOnClick) {
originalOnClick(e);
}
handleOpen();
},
});
}
return (
<div
onClick={handleOpen}
style={{ display: "inline-block", cursor: "pointer" }}
>
{trigger}
</div>
);
}
return defaultTrigger;
};
// 渲染表单内容
const renderFormContent = () => {
const currentFields = getCurrentStepFields();
return (
<>
{/* 隐藏字段始终渲染 */}
{hiddenItems.map((item) => (
<div key={item.name} style={{ display: "none" }}>
{item.element}
</div>
))}
{/* 可见字段根据当前步骤显示/隐藏 */}
{visibleItems.map((item) => {
const isCurrentStep = currentFields.includes(item.name);
return (
<div
key={item.name}
style={{ display: isCurrentStep ? "block" : "none" }}
>
{item.element}
</div>
);
})}
</>
);
};
// 渲染底部按钮
const renderFooter = () => {
if (!isMultiStep) {
// 单步骤模式
return [
<Button key="cancel" onClick={handleCancel}>
{t("cancel")}
</Button>,
<Button
key="submit"
type="primary"
loading={isPending}
onClick={handleSubmit}
>
{isEditMode() ? t("save") : t("add")}
</Button>,
];
}
// 多步骤模式(无取消按钮)
const isFirstStep = currentStep === 0;
const isLastStep = currentStep === totalSteps - 1;
return [
!isFirstStep && (
<Button key="prev" onClick={handlePrev}>
{t("prev_step")}
</Button>
),
isLastStep ? (
<Button
key="submit"
type="primary"
loading={isPending}
onClick={handleSubmit}
>
{isEditMode() ? t("save") : t("add")}
</Button>
) : (
<Button key="next" type="primary" onClick={handleNext}>
{t("next_step")}
</Button>
),
].filter(Boolean);
};
return (
<>
{renderTrigger()}
<Modal
title={title}
open={open}
onCancel={handleCancel}
width={width}
footer={renderFooter()}
destroyOnHidden={false}
maskClosable={false}
>
{description && (
<p className="text-gray-500 text-sm mb-4">{description}</p>
)}
<Form form={form} layout="vertical" size="large">
{renderFormContent()}
</Form>
</Modal>
</>
);
}