"use client"; import React, { useState, useCallback, useEffect, useRef } from "react"; import { AdminPanelConfig, AdminPanelState, UseAdminPanelOptions, UseAdminPanelReturn, FieldConfig, ValidationRule } from "@/types/admin-panel"; // 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; // State const [state, setState] = useState({ values: initialValues, errors: {}, dirty: {}, loading: false, saving: false, }); // Auto-save timer const autoSaveTimer = useRef(null); const lastSavedValues = useRef>(initialValues); // Calculate initial values with memoization to prevent unnecessary recalculations const computedInitialValues = React.useMemo(() => { const fields = getAllFields(config); const values: Record = { ...initialValues }; fields.forEach(field => { if (getNestedValue(values, field.id) === undefined) { setNestedValue(values, field.id, field.value); } }); return values; }, [config, initialValues]); // Initialize values only when computed initial values change useEffect(() => { setState(prev => { // Only update if values are actually different to prevent loops const currentJson = JSON.stringify(prev.values); const newJson = JSON.stringify(computedInitialValues); if (currentJson !== newJson) { lastSavedValues.current = computedInitialValues; return { ...prev, values: computedInitialValues }; } return prev; }); }, [computedInitialValues]); // Depend directly on memoized values // Auto-save functionality useEffect(() => { if (!config.autoSave || !onSubmit) return; const delay = config.autoSaveDelay || 2000; if (autoSaveTimer.current) { clearTimeout(autoSaveTimer.current); } // Check if values have changed const hasChanges = JSON.stringify(state.values) !== JSON.stringify(lastSavedValues.current); if (hasChanges && Object.keys(state.dirty).length > 0) { autoSaveTimer.current = setTimeout(async () => { try { await onSubmit(state.values); lastSavedValues.current = state.values; setState(prev => ({ ...prev, dirty: {} })); } catch (error) { console.error('Auto-save failed:', error); } }, delay); } return () => { if (autoSaveTimer.current) { clearTimeout(autoSaveTimer.current); } }; }, [state.values, state.dirty, config.autoSave, config.autoSaveDelay, onSubmit]); // Actions const setValue = useCallback((path: string, value: any) => { setState(prev => { const newValues = setNestedValue(prev.values, path, value); const newDirty = { ...prev.dirty, [path]: true }; // Clear error for this field const newErrors = { ...prev.errors }; delete newErrors[path]; // Validate on change if enabled let validationErrors = newErrors; if (config.validateOnChange) { const fields = getAllFields(config); const field = fields.find(f => f.id === path); if (field) { const error = validateField(field, value); if (error) { validationErrors = { ...validationErrors, [path]: error }; } } } // Call onChange callback if (config.onValueChange) { config.onValueChange(path, value, newValues); } return { ...prev, values: newValues, dirty: newDirty, errors: validationErrors, }; }); }, [config]); const setValues = useCallback((values: Record) => { setState(prev => ({ ...prev, values: { ...prev.values, ...values }, dirty: Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), prev.dirty), })); }, []); const resetValue = useCallback((path: string) => { const fields = getAllFields(config); const field = fields.find(f => f.id === path); if (field) { setValue(path, field.value); setState(prev => { const newDirty = { ...prev.dirty }; delete newDirty[path]; const newErrors = { ...prev.errors }; delete newErrors[path]; return { ...prev, dirty: newDirty, errors: newErrors }; }); } }, [config, setValue]); const resetAll = useCallback(() => { setState(prev => ({ ...prev, values: computedInitialValues, dirty: {}, errors: {}, })); if (config.onReset) { config.onReset(); } }, [computedInitialValues, config]); const validate = useCallback((): boolean => { const fields = getAllFields(config); const errors: Record = {}; fields.forEach(field => { // Skip validation for disabled or readOnly fields if (field.disabled || field.readOnly) return; // Check conditional rendering if (field.showWhen && !field.showWhen(state.values)) return; const value = getNestedValue(state.values, field.id); const error = validateField(field, value); if (error) { errors[field.id] = error; } }); // Custom validation if (config.onValidate) { const customErrors = config.onValidate(state.values); Object.assign(errors, customErrors); } setState(prev => ({ ...prev, errors })); return Object.keys(errors).length === 0; }, [config, state.values]); const save = useCallback(async () => { if (!onSubmit) return; // Validate if required if (config.validateOnSubmit !== false) { const isValid = validate(); if (!isValid) return; } setState(prev => ({ ...prev, saving: true })); try { await onSubmit(state.values); lastSavedValues.current = state.values; setState(prev => ({ ...prev, saving: false, dirty: {}, errors: {} })); if (config.onSave) { await config.onSave(state.values); } } catch (error) { setState(prev => ({ ...prev, saving: false })); throw error; } }, [config, state.values, onSubmit, validate]); const clearErrors = useCallback(() => { setState(prev => ({ ...prev, errors: {} })); }, []); // Helpers const getValue = useCallback((path: string) => { return getNestedValue(state.values, path); }, [state.values]); const getError = useCallback((path: string) => { return state.errors[path]; }, [state.errors]); const isDirty = useCallback((path?: string) => { if (path) { return Boolean(state.dirty[path]); } return Object.keys(state.dirty).length > 0; }, [state.dirty]); const isValid = useCallback((path?: string) => { if (path) { return !state.errors[path]; } return Object.keys(state.errors).length === 0; }, [state.errors]); return { state, actions: { setValue, setValues, resetValue, resetAll, save, validate, clearErrors, }, helpers: { getValue, getError, isDirty, isValid, }, }; }