mosaicmap/hooks/use-admin-panel.ts
2025-08-26 11:42:24 +08:00

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