From f37e6dac9346cf9d60adb2856cd016e9422fa5d0 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 7 Mar 2026 14:50:07 +0800 Subject: [PATCH] refactor(config): extract visual editor blocks --- src/components/config/VisualConfigEditor.tsx | 763 +---------------- .../config/VisualConfigEditorBlocks.tsx | 774 ++++++++++++++++++ 2 files changed, 779 insertions(+), 758 deletions(-) create mode 100644 src/components/config/VisualConfigEditorBlocks.tsx diff --git a/src/components/config/VisualConfigEditor.tsx b/src/components/config/VisualConfigEditor.tsx index e397205..9695c3d 100644 --- a/src/components/config/VisualConfigEditor.tsx +++ b/src/components/config/VisualConfigEditor.tsx @@ -1,33 +1,22 @@ -import { memo, useCallback, useId, useMemo, useState, type ReactNode } from 'react'; +import { useCallback, useId, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; -import { Modal } from '@/components/ui/Modal'; import { Select } from '@/components/ui/Select'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { ConfigSection } from '@/components/config/ConfigSection'; -import { useNotificationStore } from '@/stores'; -import styles from './VisualConfigEditor.module.scss'; -import { copyToClipboard } from '@/utils/clipboard'; import type { PayloadFilterRule, - PayloadModelEntry, - PayloadParamEntry, PayloadParamValidationErrorCode, - PayloadParamValueType, PayloadRule, VisualConfigValidationErrorCode, VisualConfigValidationErrors, VisualConfigValues, } from '@/types/visualConfig'; -import { makeClientId } from '@/types/visualConfig'; import { - getPayloadParamValidationError, - VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS, - VISUAL_CONFIG_PROTOCOL_OPTIONS, -} from '@/hooks/useVisualConfig'; -import { maskApiKey } from '@/utils/format'; -import { isValidApiKeyCharset } from '@/utils/validation'; + ApiKeysCardEditor, + PayloadFilterRulesEditor, + PayloadRulesEditor, +} from './VisualConfigEditorBlocks'; interface VisualConfigEditorProps { values: VisualConfigValues; @@ -94,748 +83,6 @@ function Divider() { return
; } -const ApiKeysCardEditor = memo(function ApiKeysCardEditor({ - value, - disabled, - onChange, -}: { - value: string; - disabled?: boolean; - onChange: (nextValue: string) => void; -}) { - const { t } = useTranslation(); - const showNotification = useNotificationStore((state) => state.showNotification); - const apiKeys = useMemo( - () => - value - .split('\n') - .map((key) => key.trim()) - .filter(Boolean), - [value] - ); - const [apiKeyIds, setApiKeyIds] = useState(() => apiKeys.map(() => makeClientId())); - const renderApiKeyIds = useMemo(() => { - if (apiKeyIds.length === apiKeys.length) return apiKeyIds; - if (apiKeyIds.length > apiKeys.length) return apiKeyIds.slice(0, apiKeys.length); - return [...apiKeyIds, ...Array.from({ length: apiKeys.length - apiKeyIds.length }, () => makeClientId())]; - }, [apiKeyIds, apiKeys.length]); - - const apiKeyInputId = useId(); - const apiKeyHintId = `${apiKeyInputId}-hint`; - const apiKeyErrorId = `${apiKeyInputId}-error`; - const [modalOpen, setModalOpen] = useState(false); - const [editingApiKeyId, setEditingApiKeyId] = useState(null); - const [inputValue, setInputValue] = useState(''); - const [formError, setFormError] = useState(''); - - function generateSecureApiKey(): string { - const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const array = new Uint8Array(17); - crypto.getRandomValues(array); - return 'sk-' + Array.from(array, (b) => charset[b % charset.length]).join(''); - } - - const openAddModal = () => { - setEditingApiKeyId(null); - setInputValue(''); - setFormError(''); - setModalOpen(true); - }; - - const openEditModal = (apiKeyId: string) => { - const editingIndex = renderApiKeyIds.findIndex((id) => id === apiKeyId); - setEditingApiKeyId(apiKeyId); - setInputValue(apiKeys[editingIndex] ?? ''); - setFormError(''); - setModalOpen(true); - }; - - const closeModal = () => { - setModalOpen(false); - setInputValue(''); - setEditingApiKeyId(null); - setFormError(''); - }; - - const updateApiKeys = (nextKeys: string[]) => { - onChange(nextKeys.join('\n')); - }; - - const handleDelete = (apiKeyId: string) => { - const index = renderApiKeyIds.findIndex((id) => id === apiKeyId); - if (index < 0) return; - setApiKeyIds(renderApiKeyIds.filter((id) => id !== apiKeyId)); - updateApiKeys(apiKeys.filter((_, i) => i !== index)); - }; - - const handleSave = () => { - const trimmed = inputValue.trim(); - if (!trimmed) { - setFormError(t('config_management.visual.api_keys.error_empty')); - return; - } - if (!isValidApiKeyCharset(trimmed)) { - setFormError(t('config_management.visual.api_keys.error_invalid')); - return; - } - - const editingIndex = editingApiKeyId ? renderApiKeyIds.findIndex((id) => id === editingApiKeyId) : -1; - const nextKeys = - editingApiKeyId === null - ? [...apiKeys, trimmed] - : apiKeys.map((key, idx) => (idx === editingIndex ? trimmed : key)); - if (editingApiKeyId === null) { - setApiKeyIds([...renderApiKeyIds, makeClientId()]); - } - updateApiKeys(nextKeys); - closeModal(); - }; - - const handleCopy = async (apiKey: string) => { - const copied = await copyToClipboard(apiKey); - showNotification( - t(copied ? 'notification.link_copied' : 'notification.copy_failed'), - copied ? 'success' : 'error' - ); - }; - - const handleGenerate = () => { - setInputValue(generateSecureApiKey()); - setFormError(''); - }; - - return ( -
-
- - -
- - {apiKeys.length === 0 ? ( -
- {t('config_management.visual.api_keys.empty')} -
- ) : ( -
- {apiKeys.map((key, index) => ( -
-
-
#{index + 1}
-
{t('config_management.visual.api_keys.input_label')}
-
{maskApiKey(String(key || ''))}
-
-
- - - -
-
- ))} -
- )} - -
{t('config_management.visual.api_keys.hint')}
- - - - - - } - > -
- -
- setInputValue(e.target.value)} - disabled={disabled} - aria-describedby={formError ? `${apiKeyErrorId} ${apiKeyHintId}` : apiKeyHintId} - aria-invalid={Boolean(formError)} - /> - -
-
{t('config_management.visual.api_keys.input_hint')}
- {formError &&
{formError}
} -
-
-
- ); -}); - -const StringListEditor = memo(function StringListEditor({ - value, - disabled, - placeholder, - onChange, -}: { - value: string[]; - disabled?: boolean; - placeholder?: string; - onChange: (next: string[]) => void; -}) { - const { t } = useTranslation(); - const items = value.length ? value : []; - const [itemIds, setItemIds] = useState(() => items.map(() => makeClientId())); - const renderItemIds = useMemo(() => { - if (itemIds.length === items.length) return itemIds; - if (itemIds.length > items.length) return itemIds.slice(0, items.length); - return [...itemIds, ...Array.from({ length: items.length - itemIds.length }, () => makeClientId())]; - }, [itemIds, items.length]); - - const updateItem = (index: number, nextValue: string) => - onChange(items.map((item, i) => (i === index ? nextValue : item))); - const addItem = () => { - setItemIds([...renderItemIds, makeClientId()]); - onChange([...items, '']); - }; - const removeItem = (index: number) => { - setItemIds(renderItemIds.filter((_, i) => i !== index)); - onChange(items.filter((_, i) => i !== index)); - }; - - return ( -
- {items.map((item, index) => ( -
- updateItem(index, e.target.value)} - disabled={disabled} - style={{ flex: 1 }} - /> - -
- ))} -
- -
-
- ); -}); - -const PayloadRulesEditor = memo(function PayloadRulesEditor({ - value, - disabled, - protocolFirst = false, - onChange, -}: { - value: PayloadRule[]; - disabled?: boolean; - protocolFirst?: boolean; - onChange: (next: PayloadRule[]) => void; -}) { - const { t } = useTranslation(); - const rules = value.length ? value : []; - const protocolOptions = useMemo( - () => - VISUAL_CONFIG_PROTOCOL_OPTIONS.map((option) => ({ - value: option.value, - label: t(option.labelKey, { defaultValue: option.defaultLabel }), - })), - [t] - ); - const payloadValueTypeOptions = useMemo( - () => - VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS.map((option) => ({ - value: option.value, - label: t(option.labelKey, { defaultValue: option.defaultLabel }), - })), - [t] - ); - const booleanValueOptions = useMemo( - () => [ - { value: 'true', label: t('config_management.visual.payload_rules.boolean_true') }, - { value: 'false', label: t('config_management.visual.payload_rules.boolean_false') }, - ], - [t] - ); - - const addRule = () => onChange([...rules, { id: makeClientId(), models: [], params: [] }]); - const removeRule = (ruleIndex: number) => onChange(rules.filter((_, i) => i !== ruleIndex)); - - const updateRule = (ruleIndex: number, patch: Partial) => - onChange(rules.map((rule, i) => (i === ruleIndex ? { ...rule, ...patch } : rule))); - - const addModel = (ruleIndex: number) => { - const rule = rules[ruleIndex]; - const nextModel: PayloadModelEntry = { id: makeClientId(), name: '', protocol: undefined }; - updateRule(ruleIndex, { models: [...rule.models, nextModel] }); - }; - - const removeModel = (ruleIndex: number, modelIndex: number) => { - const rule = rules[ruleIndex]; - updateRule(ruleIndex, { models: rule.models.filter((_, i) => i !== modelIndex) }); - }; - - const updateModel = (ruleIndex: number, modelIndex: number, patch: Partial) => { - const rule = rules[ruleIndex]; - updateRule(ruleIndex, { - models: rule.models.map((m, i) => (i === modelIndex ? { ...m, ...patch } : m)), - }); - }; - - const addParam = (ruleIndex: number) => { - const rule = rules[ruleIndex]; - const nextParam: PayloadParamEntry = { - id: makeClientId(), - path: '', - valueType: 'string', - value: '', - }; - updateRule(ruleIndex, { params: [...rule.params, nextParam] }); - }; - - const removeParam = (ruleIndex: number, paramIndex: number) => { - const rule = rules[ruleIndex]; - updateRule(ruleIndex, { params: rule.params.filter((_, i) => i !== paramIndex) }); - }; - - const updateParam = (ruleIndex: number, paramIndex: number, patch: Partial) => { - const rule = rules[ruleIndex]; - updateRule(ruleIndex, { - params: rule.params.map((p, i) => (i === paramIndex ? { ...p, ...patch } : p)), - }); - }; - - const getValuePlaceholder = (valueType: PayloadParamValueType) => { - switch (valueType) { - case 'string': - return t('config_management.visual.payload_rules.value_string'); - case 'number': - return t('config_management.visual.payload_rules.value_number'); - case 'boolean': - return t('config_management.visual.payload_rules.value_boolean'); - case 'json': - return t('config_management.visual.payload_rules.value_json'); - default: - return t('config_management.visual.payload_rules.value_default'); - } - }; - - const getParamErrorMessage = (param: PayloadParamEntry) => { - const errorCode = getPayloadParamValidationError(param); - return getValidationMessage(t, errorCode); - }; - - const renderParamValueEditor = ( - ruleIndex: number, - paramIndex: number, - param: PayloadParamEntry - ) => { - if (param.valueType === 'boolean') { - return ( -