391 lines
13 KiB
TypeScript
391 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback, useEffect, useRef } from "react";
|
|
import {
|
|
AdminPanelConfig,
|
|
AdminPanelState,
|
|
UseAdminPanelOptions,
|
|
UseAdminPanelReturn,
|
|
FieldConfig,
|
|
ValidationRule
|
|
} from "@/types/admin-panel";
|
|
import { useForm } from "react-hook-form";
|
|
import { configFormSchema, ConfigFormValues } from "@/types/config";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
// Helper function to get nested value
|
|
function getNestedValue(obj: any, path: string): any {
|
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
}
|
|
|
|
// Helper function to set nested value
|
|
function setNestedValue(obj: any, path: string, value: any): any {
|
|
const keys = path.split('.');
|
|
const result = { ...obj };
|
|
let current = result;
|
|
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
const key = keys[i];
|
|
current[key] = current[key] ? { ...current[key] } : {};
|
|
current = current[key];
|
|
}
|
|
|
|
current[keys[keys.length - 1]] = value;
|
|
return result;
|
|
}
|
|
|
|
// Helper function to validate a field
|
|
function validateField(field: FieldConfig, value: any): string | null {
|
|
if (!field.validation) return null;
|
|
|
|
const { validation } = field;
|
|
|
|
// Required validation
|
|
if (validation.required && (value === undefined || value === null || value === '')) {
|
|
return `${field.label}是必填项`;
|
|
}
|
|
|
|
// Skip other validations if value is empty and not required
|
|
if (value === undefined || value === null || value === '') {
|
|
return null;
|
|
}
|
|
|
|
// Min/Max validation for numbers
|
|
if (typeof value === 'number') {
|
|
if (validation.min !== undefined && value < validation.min) {
|
|
return `${field.label}不能小于${validation.min}`;
|
|
}
|
|
if (validation.max !== undefined && value > validation.max) {
|
|
return `${field.label}不能大于${validation.max}`;
|
|
}
|
|
}
|
|
|
|
// Length validation for strings
|
|
if (typeof value === 'string') {
|
|
if (validation.minLength !== undefined && value.length < validation.minLength) {
|
|
return `${field.label}长度不能少于${validation.minLength}个字符`;
|
|
}
|
|
if (validation.maxLength !== undefined && value.length > validation.maxLength) {
|
|
return `${field.label}长度不能超过${validation.maxLength}个字符`;
|
|
}
|
|
}
|
|
|
|
// Pattern validation
|
|
if (validation.pattern && typeof value === 'string' && !validation.pattern.test(value)) {
|
|
return `${field.label}格式不正确`;
|
|
}
|
|
|
|
// Custom validation
|
|
if (validation.custom) {
|
|
return validation.custom(value);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Get all fields from config
|
|
function getAllFields(config: AdminPanelConfig): FieldConfig[] {
|
|
return config.tabs.flatMap(tab =>
|
|
tab.sections.flatMap(section => section.fields)
|
|
);
|
|
}
|
|
|
|
export function useAdminPanel(options: UseAdminPanelOptions): UseAdminPanelReturn {
|
|
const { config, initialValues = {}, onSubmit } = options;
|
|
|
|
// 计算初始值,包含字段默认值
|
|
const computedInitialValues = React.useMemo(() => {
|
|
const fields = getAllFields(config);
|
|
const values: Record<string, any> = { ...initialValues };
|
|
|
|
fields.forEach(field => {
|
|
if (getNestedValue(values, field.id) === undefined) {
|
|
setNestedValue(values, field.id, field.value);
|
|
}
|
|
});
|
|
|
|
return values;
|
|
}, [config, initialValues]);
|
|
|
|
// 使用 react-hook-form 作为唯一的数据源
|
|
const form = useForm<ConfigFormValues>({
|
|
resolver: zodResolver(configFormSchema),
|
|
defaultValues: computedInitialValues,
|
|
});
|
|
|
|
// 通过 form.watch() 监听所有表单数据变化
|
|
const values = form.watch();
|
|
|
|
// 简化的状态,只保留非表单数据
|
|
const [state, setState] = useState<Omit<AdminPanelState, 'values' | 'dirty'>>({
|
|
errors: {},
|
|
loading: false,
|
|
saving: false,
|
|
});
|
|
|
|
// Auto-save timer 和上次保存的值
|
|
const autoSaveTimer = useRef<NodeJS.Timeout | null>(null);
|
|
const lastSavedValues = useRef<Record<string, any>>(computedInitialValues);
|
|
|
|
// 使用 form.formState.dirtyFields 来判断字段是否已修改
|
|
const { formState } = form;
|
|
const { dirtyFields } = formState;
|
|
|
|
// 重新设置初始值(当配置或初始值变化时)
|
|
useEffect(() => {
|
|
form.reset(computedInitialValues);
|
|
lastSavedValues.current = computedInitialValues;
|
|
}, [computedInitialValues]); // 移除 form 依赖
|
|
|
|
// Auto-save 功能
|
|
useEffect(() => {
|
|
if (!config.autoSave || !onSubmit) return;
|
|
|
|
const delay = config.autoSaveDelay || 2000;
|
|
|
|
if (autoSaveTimer.current) {
|
|
clearTimeout(autoSaveTimer.current);
|
|
}
|
|
|
|
// 检查是否有变化
|
|
const hasChanges = JSON.stringify(values) !== JSON.stringify(lastSavedValues.current);
|
|
const hasDirtyFields = Object.keys(dirtyFields).length > 0;
|
|
|
|
if (hasChanges && hasDirtyFields) {
|
|
autoSaveTimer.current = setTimeout(async () => {
|
|
try {
|
|
const currentValues = form.getValues();
|
|
await onSubmit(currentValues);
|
|
lastSavedValues.current = currentValues;
|
|
// 标记所有字段为干净状态,但不改变值
|
|
form.reset(currentValues, { keepValues: true });
|
|
} catch (error) {
|
|
console.error('Auto-save failed:', error);
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
return () => {
|
|
if (autoSaveTimer.current) {
|
|
clearTimeout(autoSaveTimer.current);
|
|
}
|
|
};
|
|
}, [values, dirtyFields, config.autoSave, config.autoSaveDelay, onSubmit]); // 移除 form 依赖
|
|
|
|
// 缓存字段配置以避免循环依赖
|
|
const fields = React.useMemo(() => getAllFields(config), [config]);
|
|
const validateOnChange = config.validateOnChange;
|
|
|
|
// 防抖验证,避免频繁验证
|
|
const validationTimer = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// 执行字段验证的函数
|
|
const performValidation = useCallback(() => {
|
|
const errors: Record<string, string> = {};
|
|
|
|
fields.forEach(field => {
|
|
// 跳过禁用或只读字段的验证
|
|
if (field.disabled || field.readOnly) return;
|
|
|
|
// 检查条件渲染
|
|
if (field.showWhen && !field.showWhen(values)) return;
|
|
|
|
const value = getNestedValue(values, field.id);
|
|
const error = validateField(field, value);
|
|
if (error) {
|
|
errors[field.id] = error;
|
|
}
|
|
});
|
|
|
|
setState(prev => ({ ...prev, errors }));
|
|
}, [fields, values]);
|
|
|
|
// 初始验证 - 组件挂载时和配置变化时执行一次
|
|
useEffect(() => {
|
|
performValidation();
|
|
}, [fields, computedInitialValues]); // 当字段配置或初始值变化时重新验证
|
|
|
|
// 监听值变化进行实时验证
|
|
useEffect(() => {
|
|
if (!validateOnChange) return;
|
|
|
|
// 清除之前的验证定时器
|
|
if (validationTimer.current) {
|
|
clearTimeout(validationTimer.current);
|
|
}
|
|
|
|
// 延迟验证,避免频繁触发
|
|
validationTimer.current = setTimeout(() => {
|
|
performValidation();
|
|
}, 300); // 300ms 防抖
|
|
|
|
return () => {
|
|
if (validationTimer.current) {
|
|
clearTimeout(validationTimer.current);
|
|
}
|
|
};
|
|
}, [values, validateOnChange, performValidation]);
|
|
|
|
// Actions - 使用 form 的方法来更新值
|
|
const setValue = useCallback((path: string, value: any) => {
|
|
(form.setValue as any)(path, value, { shouldDirty: true, shouldValidate: validateOnChange });
|
|
|
|
// 清除该字段的错误
|
|
setState(prev => {
|
|
const newErrors = { ...prev.errors };
|
|
delete newErrors[path];
|
|
return { ...prev, errors: newErrors };
|
|
});
|
|
|
|
// 调用 onChange 回调
|
|
if (config.onValueChange) {
|
|
const currentValues = form.getValues();
|
|
const newValues = setNestedValue(currentValues, path, value);
|
|
config.onValueChange(path, value, newValues);
|
|
}
|
|
}, [form, config.onValueChange, validateOnChange]); // 移除 values 依赖
|
|
|
|
const setValues = useCallback((newValues: Record<string, any>) => {
|
|
Object.entries(newValues).forEach(([path, value]) => {
|
|
(form.setValue as any)(path, value, { shouldDirty: true });
|
|
});
|
|
}, [form]);
|
|
|
|
const resetValue = useCallback((path: string) => {
|
|
const field = fields.find(f => f.id === path);
|
|
if (field) {
|
|
(form.setValue as any)(path, field.value, { shouldDirty: false });
|
|
|
|
// 清除该字段的错误
|
|
setState(prev => {
|
|
const newErrors = { ...prev.errors };
|
|
delete newErrors[path];
|
|
return { ...prev, errors: newErrors };
|
|
});
|
|
}
|
|
}, [fields, form]);
|
|
|
|
const resetAll = useCallback(() => {
|
|
form.reset(computedInitialValues);
|
|
setState(prev => ({ ...prev, errors: {} }));
|
|
lastSavedValues.current = computedInitialValues;
|
|
|
|
if (config.onReset) {
|
|
config.onReset();
|
|
}
|
|
}, [computedInitialValues, config.onReset, form]);
|
|
|
|
const validate = useCallback((): boolean => {
|
|
const currentValues = form.getValues();
|
|
const errors: Record<string, string> = {};
|
|
|
|
fields.forEach(field => {
|
|
// 跳过禁用或只读字段的验证
|
|
if (field.disabled || field.readOnly) return;
|
|
|
|
// 检查条件渲染
|
|
if (field.showWhen && !field.showWhen(currentValues)) return;
|
|
|
|
const value = getNestedValue(currentValues, field.id);
|
|
const error = validateField(field, value);
|
|
if (error) {
|
|
errors[field.id] = error;
|
|
}
|
|
});
|
|
|
|
// 自定义验证
|
|
if (config.onValidate) {
|
|
const customErrors = config.onValidate(currentValues);
|
|
Object.assign(errors, customErrors);
|
|
}
|
|
|
|
setState(prev => ({ ...prev, errors }));
|
|
return Object.keys(errors).length === 0;
|
|
}, [fields, config.onValidate, form]);
|
|
|
|
const save = useCallback(async () => {
|
|
if (!onSubmit) return;
|
|
|
|
// 验证(如果需要)
|
|
// if (config.validateOnSubmit !== false) {
|
|
// const isValid = validate();
|
|
// if (!isValid) return;
|
|
// }
|
|
|
|
setState(prev => ({ ...prev, saving: true }));
|
|
const currentValues = form.getValues();
|
|
|
|
try {
|
|
await onSubmit(currentValues);
|
|
lastSavedValues.current = currentValues;
|
|
// 标记所有字段为干净状态,但不改变值
|
|
form.reset(currentValues, { keepValues: true });
|
|
setState(prev => ({
|
|
...prev,
|
|
saving: false,
|
|
errors: {}
|
|
}));
|
|
|
|
if (config.onSave) {
|
|
await config.onSave(currentValues);
|
|
}
|
|
} catch (error) {
|
|
setState(prev => ({ ...prev, saving: false }));
|
|
throw error;
|
|
}
|
|
}, [config.validateOnSubmit, config.onSave, onSubmit, validate, form]);
|
|
|
|
const clearErrors = useCallback(() => {
|
|
setState(prev => ({ ...prev, errors: {} }));
|
|
}, []);
|
|
|
|
// Helpers - 直接使用 form.watch() 的数据
|
|
const getValue = useCallback((path: string) => {
|
|
return getNestedValue(values, path);
|
|
}, [values]);
|
|
|
|
const getError = useCallback((path: string) => {
|
|
return state.errors[path] || getNestedValue(form.formState.errors, path)?.message;
|
|
}, [state.errors, form.formState.errors]);
|
|
|
|
const isDirty = useCallback((path?: string) => {
|
|
if (path) {
|
|
return Boolean(getNestedValue(dirtyFields, path));
|
|
}
|
|
return Object.keys(dirtyFields).length > 0;
|
|
}, [dirtyFields]);
|
|
|
|
const isValid = useCallback((path?: string) => {
|
|
if (path) {
|
|
return !state.errors[path] && !getNestedValue(form.formState.errors, path);
|
|
}
|
|
return Object.keys(state.errors).length === 0 && form.formState.isValid;
|
|
}, [state.errors, form.formState.errors, form.formState.isValid]);
|
|
|
|
// 构建返回的状态,包含 values
|
|
const adminPanelState: AdminPanelState = {
|
|
...state,
|
|
values,
|
|
dirty: dirtyFields,
|
|
};
|
|
|
|
return {
|
|
form,
|
|
state: adminPanelState,
|
|
actions: {
|
|
setValue,
|
|
setValues,
|
|
resetValue,
|
|
resetAll,
|
|
save,
|
|
validate,
|
|
clearErrors,
|
|
},
|
|
helpers: {
|
|
getValue,
|
|
getError,
|
|
isDirty,
|
|
isValid,
|
|
},
|
|
};
|
|
} |