| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609 |
- import React, { useState, useEffect, useCallback } from 'react';
- import { useTranslation } from 'react-i18next';
- import {
- Space,
- Button,
- Form,
- Card,
- Typography,
- Banner,
- Row,
- Col,
- InputNumber,
- Switch,
- Select,
- Input,
- } from '@douyinfe/semi-ui';
- import {
- IconCode,
- IconEdit,
- IconPlus,
- IconDelete,
- IconSetting,
- } from '@douyinfe/semi-icons';
- const { Text } = Typography;
- const JSONEditor = ({
- value = '',
- onChange,
- field,
- label,
- placeholder,
- extraText,
- showClear = true,
- template,
- templateLabel,
- editorType = 'keyValue', // keyValue, object, region
- autosize = true,
- rules = [],
- formApi = null,
- ...props
- }) => {
- const { t } = useTranslation();
-
- // 初始化JSON数据
- const [jsonData, setJsonData] = useState(() => {
- // 初始化时解析JSON数据
- if (value && value.trim()) {
- try {
- const parsed = JSON.parse(value);
- return parsed;
- } catch (error) {
- return {};
- }
- }
- return {};
- });
-
- // 根据键数量决定默认编辑模式
- const [editMode, setEditMode] = useState(() => {
- // 如果初始JSON数据的键数量大于10个,则默认使用手动模式
- if (value && value.trim()) {
- try {
- const parsed = JSON.parse(value);
- const keyCount = Object.keys(parsed).length;
- return keyCount > 10 ? 'manual' : 'visual';
- } catch (error) {
- return 'visual';
- }
- }
- return 'visual';
- });
- const [jsonError, setJsonError] = useState('');
- // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
- useEffect(() => {
- try {
- const parsed = value && value.trim() ? JSON.parse(value) : {};
- setJsonData(parsed);
- setJsonError('');
- } catch (error) {
- console.log('JSON解析失败:', error.message);
- setJsonError(error.message);
- // JSON格式错误时不更新jsonData
- }
- }, [value]);
- // 处理可视化编辑的数据变化
- const handleVisualChange = useCallback((newData) => {
- setJsonData(newData);
- setJsonError('');
- const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
-
- // 通过formApi设置值(如果提供的话)
- if (formApi && field) {
- formApi.setValue(field, jsonString);
- }
-
- onChange?.(jsonString);
- }, [onChange, formApi, field]);
- // 处理手动编辑的数据变化
- const handleManualChange = useCallback((newValue) => {
- onChange?.(newValue);
- // 验证JSON格式
- if (newValue && newValue.trim()) {
- try {
- const parsed = JSON.parse(newValue);
- setJsonError('');
- // 预先准备可视化数据,但不立即应用
- // 这样切换到可视化模式时数据已经准备好了
- } catch (error) {
- setJsonError(error.message);
- }
- } else {
- setJsonError('');
- }
- }, [onChange]);
- // 切换编辑模式
- const toggleEditMode = useCallback(() => {
- if (editMode === 'visual') {
- // 从可视化模式切换到手动模式
- setEditMode('manual');
- } else {
- // 从手动模式切换到可视化模式,需要验证JSON
- try {
- const parsed = value && value.trim() ? JSON.parse(value) : {};
- setJsonData(parsed);
- setJsonError('');
- setEditMode('visual');
- } catch (error) {
- setJsonError(error.message);
- // JSON格式错误时不切换模式
- return;
- }
- }
- }, [editMode, value]);
- // 添加键值对
- const addKeyValue = useCallback(() => {
- const newData = { ...jsonData };
- const keys = Object.keys(newData);
- let newKey = 'key';
- let counter = 1;
- while (newData.hasOwnProperty(newKey)) {
- newKey = `key${counter}`;
- counter++;
- }
- newData[newKey] = '';
- handleVisualChange(newData);
- }, [jsonData, handleVisualChange]);
- // 删除键值对
- const removeKeyValue = useCallback((keyToRemove) => {
- const newData = { ...jsonData };
- delete newData[keyToRemove];
- handleVisualChange(newData);
- }, [jsonData, handleVisualChange]);
- // 更新键名
- const updateKey = useCallback((oldKey, newKey) => {
- if (oldKey === newKey) return;
- const newData = { ...jsonData };
- const value = newData[oldKey];
- delete newData[oldKey];
- newData[newKey] = value;
- handleVisualChange(newData);
- }, [jsonData, handleVisualChange]);
- // 更新值
- const updateValue = useCallback((key, newValue) => {
- const newData = { ...jsonData };
- newData[key] = newValue;
- handleVisualChange(newData);
- }, [jsonData, handleVisualChange]);
- // 填入模板
- const fillTemplate = useCallback(() => {
- if (template) {
- const templateString = JSON.stringify(template, null, 2);
-
- // 通过formApi设置值(如果提供的话)
- if (formApi && field) {
- formApi.setValue(field, templateString);
- }
-
- // 无论哪种模式都要更新值
- onChange?.(templateString);
-
- // 如果是可视化模式,同时更新jsonData
- if (editMode === 'visual') {
- setJsonData(template);
- }
-
- // 清除错误状态
- setJsonError('');
- }
- }, [template, onChange, editMode, formApi, field]);
- // 渲染键值对编辑器
- const renderKeyValueEditor = () => {
- const entries = Object.entries(jsonData);
-
- return (
- <div className="space-y-1">
- {entries.length === 0 && (
- <div className="text-center py-6 px-4">
- <div className="text-gray-400 mb-2">
- <IconCode size={32} />
- </div>
- <Text type="tertiary" className="text-gray-500 text-sm">
- {t('暂无数据,点击下方按钮添加键值对')}
- </Text>
- </div>
- )}
-
- {entries.map(([key, value], index) => (
- <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
- <Row gutter={12} align="middle">
- <Col span={10}>
- <div className="space-y-1">
- <Text type="tertiary" size="small">{t('键名')}</Text>
- <Input
- placeholder={t('键名')}
- value={key}
- onChange={(newKey) => updateKey(key, newKey)}
- size="small"
- />
- </div>
- </Col>
- <Col span={11}>
- <div className="space-y-1">
- <Text type="tertiary" size="small">{t('值')}</Text>
- <Input
- placeholder={t('值')}
- value={value}
- onChange={(newValue) => updateValue(key, newValue)}
- size="small"
- />
- </div>
- </Col>
- <Col span={3}>
- <div className="flex justify-center pt-4">
- <Button
- icon={<IconDelete />}
- type="danger"
- theme="borderless"
- size="small"
- onClick={() => removeKeyValue(key)}
- className="hover:bg-red-50"
- />
- </div>
- </Col>
- </Row>
- </Card>
- ))}
-
- <div className="flex justify-center pt-1">
- <Button
- icon={<IconPlus />}
- onClick={addKeyValue}
- size="small"
- theme="solid"
- type="primary"
- className="shadow-sm hover:shadow-md transition-shadow px-4"
- >
- {t('添加键值对')}
- </Button>
- </div>
- </div>
- );
- };
- // 渲染对象编辑器(用于复杂JSON)
- const renderObjectEditor = () => {
- const entries = Object.entries(jsonData);
-
- return (
- <div className="space-y-1">
- {entries.length === 0 && (
- <div className="text-center py-6 px-4">
- <div className="text-gray-400 mb-2">
- <IconSetting size={32} />
- </div>
- <Text type="tertiary" className="text-gray-500 text-sm">
- {t('暂无参数,点击下方按钮添加请求参数')}
- </Text>
- </div>
- )}
-
- {entries.map(([key, value], index) => (
- <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
- <Row gutter={12} align="middle">
- <Col span={8}>
- <div className="space-y-1">
- <Text type="tertiary" size="small">{t('参数名')}</Text>
- <Input
- placeholder={t('参数名')}
- value={key}
- onChange={(newKey) => updateKey(key, newKey)}
- size="small"
- />
- </div>
- </Col>
- <Col span={13}>
- <div className="space-y-1">
- <Text type="tertiary" size="small">{t('参数值')} ({typeof value})</Text>
- {renderValueInput(key, value)}
- </div>
- </Col>
- <Col span={3}>
- <div className="flex justify-center pt-4">
- <Button
- icon={<IconDelete />}
- type="danger"
- theme="borderless"
- size="small"
- onClick={() => removeKeyValue(key)}
- className="hover:bg-red-50"
- />
- </div>
- </Col>
- </Row>
- </Card>
- ))}
-
- <div className="flex justify-center pt-1">
- <Button
- icon={<IconPlus />}
- onClick={addKeyValue}
- size="small"
- theme="solid"
- type="primary"
- className="shadow-sm hover:shadow-md transition-shadow px-4"
- >
- {t('添加参数')}
- </Button>
- </div>
- </div>
- );
- };
- // 渲染参数值输入控件
- const renderValueInput = (key, value) => {
- const valueType = typeof value;
-
- if (valueType === 'boolean') {
- return (
- <div className="flex items-center">
- <Switch
- checked={value}
- onChange={(newValue) => updateValue(key, newValue)}
- size="small"
- />
- <Text type="tertiary" size="small" className="ml-2">
- {value ? t('true') : t('false')}
- </Text>
- </div>
- );
- }
-
- if (valueType === 'number') {
- return (
- <InputNumber
- value={value}
- onChange={(newValue) => updateValue(key, newValue)}
- size="small"
- style={{ width: '100%' }}
- step={key === 'temperature' ? 0.1 : 1}
- precision={key === 'temperature' ? 2 : 0}
- placeholder={t('输入数字')}
- />
- );
- }
-
- // 字符串类型或其他类型
- return (
- <Input
- placeholder={t('参数值')}
- value={String(value)}
- onChange={(newValue) => {
- // 尝试转换为适当的类型
- let convertedValue = newValue;
- if (newValue === 'true') convertedValue = true;
- else if (newValue === 'false') convertedValue = false;
- else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
- convertedValue = Number(newValue);
- }
-
- updateValue(key, convertedValue);
- }}
- size="small"
- />
- );
- };
- // 渲染区域编辑器(特殊格式)
- const renderRegionEditor = () => {
- const entries = Object.entries(jsonData);
- const defaultEntry = entries.find(([key]) => key === 'default');
- const modelEntries = entries.filter(([key]) => key !== 'default');
-
- return (
- <div className="space-y-1">
- {/* 默认区域 */}
- <Card className="!p-2 !border-blue-200 !bg-blue-50">
- <div className="flex items-center mb-1">
- <Text strong size="small" className="text-blue-700">{t('默认区域')}</Text>
- </div>
- <Input
- placeholder={t('默认区域,如: us-central1')}
- value={defaultEntry ? defaultEntry[1] : ''}
- onChange={(value) => updateValue('default', value)}
- size="small"
- />
- </Card>
-
- {/* 模型专用区域 */}
- <div className="space-y-1">
- <Text strong size="small">{t('模型专用区域')}</Text>
- {modelEntries.map(([modelName, region], index) => (
- <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
- <Row gutter={12} align="middle">
- <Col span={10}>
- <div className="space-y-1">
- <Text type="tertiary" size="small">{t('模型名称')}</Text>
- <Input
- placeholder={t('模型名称')}
- value={modelName}
- onChange={(newKey) => updateKey(modelName, newKey)}
- size="small"
- />
- </div>
- </Col>
- <Col span={11}>
- <div className="space-y-1">
- <Text type="tertiary" size="small">{t('区域')}</Text>
- <Input
- placeholder={t('区域')}
- value={region}
- onChange={(newValue) => updateValue(modelName, newValue)}
- size="small"
- />
- </div>
- </Col>
- <Col span={3}>
- <div className="flex justify-center pt-4">
- <Button
- icon={<IconDelete />}
- type="danger"
- theme="borderless"
- size="small"
- onClick={() => removeKeyValue(modelName)}
- className="hover:bg-red-50"
- />
- </div>
- </Col>
- </Row>
- </Card>
- ))}
-
- <div className="flex justify-center pt-1">
- <Button
- icon={<IconPlus />}
- onClick={addKeyValue}
- size="small"
- theme="solid"
- type="primary"
- className="shadow-sm hover:shadow-md transition-shadow px-4"
- >
- {t('添加模型区域')}
- </Button>
- </div>
- </div>
- </div>
- );
- };
- // 渲染可视化编辑器
- const renderVisualEditor = () => {
- switch (editorType) {
- case 'region':
- return renderRegionEditor();
- case 'object':
- return renderObjectEditor();
- case 'keyValue':
- default:
- return renderKeyValueEditor();
- }
- };
- const hasJsonError = jsonError && jsonError.trim() !== '';
- return (
- <div className="space-y-1">
- {/* Label统一显示在上方 */}
- {label && (
- <div className="flex items-center">
- <Text className="text-sm font-medium text-gray-900">{label}</Text>
- </div>
- )}
-
- {/* 编辑模式切换 */}
- <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
- <div className="flex items-center gap-2">
- {editMode === 'visual' && (
- <Text type="tertiary" size="small" className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs">
- {t('可视化模式')}
- </Text>
- )}
- {editMode === 'manual' && (
- <Text type="tertiary" size="small" className="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs">
- {t('手动编辑模式')}
- </Text>
- )}
- </div>
- <div className="flex items-center gap-2">
- {template && templateLabel && (
- <Button
- size="small"
- type="tertiary"
- onClick={fillTemplate}
- className="!text-semi-color-primary hover:bg-blue-50 text-xs"
- >
- {templateLabel}
- </Button>
- )}
- <Space size="tight">
- <Button
- size="small"
- type={editMode === 'visual' ? 'primary' : 'tertiary'}
- icon={<IconEdit />}
- onClick={toggleEditMode}
- disabled={editMode === 'manual' && hasJsonError}
- className={editMode === 'visual' ? 'shadow-sm' : ''}
- >
- {t('可视化')}
- </Button>
- <Button
- size="small"
- type={editMode === 'manual' ? 'primary' : 'tertiary'}
- icon={<IconCode />}
- onClick={toggleEditMode}
- className={editMode === 'manual' ? 'shadow-sm' : ''}
- >
- {t('手动编辑')}
- </Button>
- </Space>
- </div>
- </div>
- {/* JSON错误提示 */}
- {hasJsonError && (
- <Banner
- type="danger"
- description={`JSON 格式错误: ${jsonError}`}
- className="!rounded-md text-sm"
- />
- )}
- {/* 编辑器内容 */}
- {editMode === 'visual' ? (
- <div>
- <Card className="!p-3 !border-gray-200 !shadow-sm !rounded-md bg-white">
- {renderVisualEditor()}
- </Card>
- {/* 可视化模式下的额外文本显示在下方 */}
- {extraText && (
- <div className="text-xs text-gray-600 mt-0.5">
- {extraText}
- </div>
- )}
- {/* 隐藏的Form字段用于验证和数据绑定 */}
- <Form.Input
- field={field}
- value={value}
- rules={rules}
- style={{ display: 'none' }}
- noLabel={true}
- {...props}
- />
- </div>
- ) : (
- <Form.TextArea
- field={field}
- placeholder={placeholder}
- value={value}
- onChange={handleManualChange}
- showClear={showClear}
- rows={Math.max(8, value ? value.split('\n').length : 8)}
- rules={rules}
- noLabel={true}
- {...props}
- />
- )}
- {/* 额外文本在手动编辑模式下显示 */}
- {extraText && editMode === 'manual' && (
- <div className="text-xs text-gray-600">
- {extraText}
- </div>
- )}
- </div>
- );
- };
- export default JSONEditor;
|