JSONEditor.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. import React, { useState, useEffect, useCallback } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. Space,
  5. Button,
  6. Form,
  7. Card,
  8. Typography,
  9. Banner,
  10. Row,
  11. Col,
  12. InputNumber,
  13. Switch,
  14. Select,
  15. Input,
  16. } from '@douyinfe/semi-ui';
  17. import {
  18. IconCode,
  19. IconEdit,
  20. IconPlus,
  21. IconDelete,
  22. IconSetting,
  23. } from '@douyinfe/semi-icons';
  24. const { Text } = Typography;
  25. const JSONEditor = ({
  26. value = '',
  27. onChange,
  28. field,
  29. label,
  30. placeholder,
  31. extraText,
  32. showClear = true,
  33. template,
  34. templateLabel,
  35. editorType = 'keyValue', // keyValue, object, region
  36. autosize = true,
  37. rules = [],
  38. formApi = null,
  39. ...props
  40. }) => {
  41. const { t } = useTranslation();
  42. // 初始化JSON数据
  43. const [jsonData, setJsonData] = useState(() => {
  44. // 初始化时解析JSON数据
  45. if (value && value.trim()) {
  46. try {
  47. const parsed = JSON.parse(value);
  48. return parsed;
  49. } catch (error) {
  50. return {};
  51. }
  52. }
  53. return {};
  54. });
  55. // 根据键数量决定默认编辑模式
  56. const [editMode, setEditMode] = useState(() => {
  57. // 如果初始JSON数据的键数量大于10个,则默认使用手动模式
  58. if (value && value.trim()) {
  59. try {
  60. const parsed = JSON.parse(value);
  61. const keyCount = Object.keys(parsed).length;
  62. return keyCount > 10 ? 'manual' : 'visual';
  63. } catch (error) {
  64. return 'visual';
  65. }
  66. }
  67. return 'visual';
  68. });
  69. const [jsonError, setJsonError] = useState('');
  70. // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
  71. useEffect(() => {
  72. try {
  73. const parsed = value && value.trim() ? JSON.parse(value) : {};
  74. setJsonData(parsed);
  75. setJsonError('');
  76. } catch (error) {
  77. console.log('JSON解析失败:', error.message);
  78. setJsonError(error.message);
  79. // JSON格式错误时不更新jsonData
  80. }
  81. }, [value]);
  82. // 处理可视化编辑的数据变化
  83. const handleVisualChange = useCallback((newData) => {
  84. setJsonData(newData);
  85. setJsonError('');
  86. const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
  87. // 通过formApi设置值(如果提供的话)
  88. if (formApi && field) {
  89. formApi.setValue(field, jsonString);
  90. }
  91. onChange?.(jsonString);
  92. }, [onChange, formApi, field]);
  93. // 处理手动编辑的数据变化
  94. const handleManualChange = useCallback((newValue) => {
  95. onChange?.(newValue);
  96. // 验证JSON格式
  97. if (newValue && newValue.trim()) {
  98. try {
  99. const parsed = JSON.parse(newValue);
  100. setJsonError('');
  101. // 预先准备可视化数据,但不立即应用
  102. // 这样切换到可视化模式时数据已经准备好了
  103. } catch (error) {
  104. setJsonError(error.message);
  105. }
  106. } else {
  107. setJsonError('');
  108. }
  109. }, [onChange]);
  110. // 切换编辑模式
  111. const toggleEditMode = useCallback(() => {
  112. if (editMode === 'visual') {
  113. // 从可视化模式切换到手动模式
  114. setEditMode('manual');
  115. } else {
  116. // 从手动模式切换到可视化模式,需要验证JSON
  117. try {
  118. const parsed = value && value.trim() ? JSON.parse(value) : {};
  119. setJsonData(parsed);
  120. setJsonError('');
  121. setEditMode('visual');
  122. } catch (error) {
  123. setJsonError(error.message);
  124. // JSON格式错误时不切换模式
  125. return;
  126. }
  127. }
  128. }, [editMode, value]);
  129. // 添加键值对
  130. const addKeyValue = useCallback(() => {
  131. const newData = { ...jsonData };
  132. const keys = Object.keys(newData);
  133. let newKey = 'key';
  134. let counter = 1;
  135. while (newData.hasOwnProperty(newKey)) {
  136. newKey = `key${counter}`;
  137. counter++;
  138. }
  139. newData[newKey] = '';
  140. handleVisualChange(newData);
  141. }, [jsonData, handleVisualChange]);
  142. // 删除键值对
  143. const removeKeyValue = useCallback((keyToRemove) => {
  144. const newData = { ...jsonData };
  145. delete newData[keyToRemove];
  146. handleVisualChange(newData);
  147. }, [jsonData, handleVisualChange]);
  148. // 更新键名
  149. const updateKey = useCallback((oldKey, newKey) => {
  150. if (oldKey === newKey) return;
  151. const newData = { ...jsonData };
  152. const value = newData[oldKey];
  153. delete newData[oldKey];
  154. newData[newKey] = value;
  155. handleVisualChange(newData);
  156. }, [jsonData, handleVisualChange]);
  157. // 更新值
  158. const updateValue = useCallback((key, newValue) => {
  159. const newData = { ...jsonData };
  160. newData[key] = newValue;
  161. handleVisualChange(newData);
  162. }, [jsonData, handleVisualChange]);
  163. // 填入模板
  164. const fillTemplate = useCallback(() => {
  165. if (template) {
  166. const templateString = JSON.stringify(template, null, 2);
  167. // 通过formApi设置值(如果提供的话)
  168. if (formApi && field) {
  169. formApi.setValue(field, templateString);
  170. }
  171. // 无论哪种模式都要更新值
  172. onChange?.(templateString);
  173. // 如果是可视化模式,同时更新jsonData
  174. if (editMode === 'visual') {
  175. setJsonData(template);
  176. }
  177. // 清除错误状态
  178. setJsonError('');
  179. }
  180. }, [template, onChange, editMode, formApi, field]);
  181. // 渲染键值对编辑器
  182. const renderKeyValueEditor = () => {
  183. const entries = Object.entries(jsonData);
  184. return (
  185. <div className="space-y-1">
  186. {entries.length === 0 && (
  187. <div className="text-center py-6 px-4">
  188. <div className="text-gray-400 mb-2">
  189. <IconCode size={32} />
  190. </div>
  191. <Text type="tertiary" className="text-gray-500 text-sm">
  192. {t('暂无数据,点击下方按钮添加键值对')}
  193. </Text>
  194. </div>
  195. )}
  196. {entries.map(([key, value], index) => (
  197. <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
  198. <Row gutter={12} align="middle">
  199. <Col span={10}>
  200. <div className="space-y-1">
  201. <Text type="tertiary" size="small">{t('键名')}</Text>
  202. <Input
  203. placeholder={t('键名')}
  204. value={key}
  205. onChange={(newKey) => updateKey(key, newKey)}
  206. size="small"
  207. />
  208. </div>
  209. </Col>
  210. <Col span={11}>
  211. <div className="space-y-1">
  212. <Text type="tertiary" size="small">{t('值')}</Text>
  213. <Input
  214. placeholder={t('值')}
  215. value={value}
  216. onChange={(newValue) => updateValue(key, newValue)}
  217. size="small"
  218. />
  219. </div>
  220. </Col>
  221. <Col span={3}>
  222. <div className="flex justify-center pt-4">
  223. <Button
  224. icon={<IconDelete />}
  225. type="danger"
  226. theme="borderless"
  227. size="small"
  228. onClick={() => removeKeyValue(key)}
  229. className="hover:bg-red-50"
  230. />
  231. </div>
  232. </Col>
  233. </Row>
  234. </Card>
  235. ))}
  236. <div className="flex justify-center pt-1">
  237. <Button
  238. icon={<IconPlus />}
  239. onClick={addKeyValue}
  240. size="small"
  241. theme="solid"
  242. type="primary"
  243. className="shadow-sm hover:shadow-md transition-shadow px-4"
  244. >
  245. {t('添加键值对')}
  246. </Button>
  247. </div>
  248. </div>
  249. );
  250. };
  251. // 渲染对象编辑器(用于复杂JSON)
  252. const renderObjectEditor = () => {
  253. const entries = Object.entries(jsonData);
  254. return (
  255. <div className="space-y-1">
  256. {entries.length === 0 && (
  257. <div className="text-center py-6 px-4">
  258. <div className="text-gray-400 mb-2">
  259. <IconSetting size={32} />
  260. </div>
  261. <Text type="tertiary" className="text-gray-500 text-sm">
  262. {t('暂无参数,点击下方按钮添加请求参数')}
  263. </Text>
  264. </div>
  265. )}
  266. {entries.map(([key, value], index) => (
  267. <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
  268. <Row gutter={12} align="middle">
  269. <Col span={8}>
  270. <div className="space-y-1">
  271. <Text type="tertiary" size="small">{t('参数名')}</Text>
  272. <Input
  273. placeholder={t('参数名')}
  274. value={key}
  275. onChange={(newKey) => updateKey(key, newKey)}
  276. size="small"
  277. />
  278. </div>
  279. </Col>
  280. <Col span={13}>
  281. <div className="space-y-1">
  282. <Text type="tertiary" size="small">{t('参数值')} ({typeof value})</Text>
  283. {renderValueInput(key, value)}
  284. </div>
  285. </Col>
  286. <Col span={3}>
  287. <div className="flex justify-center pt-4">
  288. <Button
  289. icon={<IconDelete />}
  290. type="danger"
  291. theme="borderless"
  292. size="small"
  293. onClick={() => removeKeyValue(key)}
  294. className="hover:bg-red-50"
  295. />
  296. </div>
  297. </Col>
  298. </Row>
  299. </Card>
  300. ))}
  301. <div className="flex justify-center pt-1">
  302. <Button
  303. icon={<IconPlus />}
  304. onClick={addKeyValue}
  305. size="small"
  306. theme="solid"
  307. type="primary"
  308. className="shadow-sm hover:shadow-md transition-shadow px-4"
  309. >
  310. {t('添加参数')}
  311. </Button>
  312. </div>
  313. </div>
  314. );
  315. };
  316. // 渲染参数值输入控件
  317. const renderValueInput = (key, value) => {
  318. const valueType = typeof value;
  319. if (valueType === 'boolean') {
  320. return (
  321. <div className="flex items-center">
  322. <Switch
  323. checked={value}
  324. onChange={(newValue) => updateValue(key, newValue)}
  325. size="small"
  326. />
  327. <Text type="tertiary" size="small" className="ml-2">
  328. {value ? t('true') : t('false')}
  329. </Text>
  330. </div>
  331. );
  332. }
  333. if (valueType === 'number') {
  334. return (
  335. <InputNumber
  336. value={value}
  337. onChange={(newValue) => updateValue(key, newValue)}
  338. size="small"
  339. style={{ width: '100%' }}
  340. step={key === 'temperature' ? 0.1 : 1}
  341. precision={key === 'temperature' ? 2 : 0}
  342. placeholder={t('输入数字')}
  343. />
  344. );
  345. }
  346. // 字符串类型或其他类型
  347. return (
  348. <Input
  349. placeholder={t('参数值')}
  350. value={String(value)}
  351. onChange={(newValue) => {
  352. // 尝试转换为适当的类型
  353. let convertedValue = newValue;
  354. if (newValue === 'true') convertedValue = true;
  355. else if (newValue === 'false') convertedValue = false;
  356. else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
  357. convertedValue = Number(newValue);
  358. }
  359. updateValue(key, convertedValue);
  360. }}
  361. size="small"
  362. />
  363. );
  364. };
  365. // 渲染区域编辑器(特殊格式)
  366. const renderRegionEditor = () => {
  367. const entries = Object.entries(jsonData);
  368. const defaultEntry = entries.find(([key]) => key === 'default');
  369. const modelEntries = entries.filter(([key]) => key !== 'default');
  370. return (
  371. <div className="space-y-1">
  372. {/* 默认区域 */}
  373. <Card className="!p-2 !border-blue-200 !bg-blue-50">
  374. <div className="flex items-center mb-1">
  375. <Text strong size="small" className="text-blue-700">{t('默认区域')}</Text>
  376. </div>
  377. <Input
  378. placeholder={t('默认区域,如: us-central1')}
  379. value={defaultEntry ? defaultEntry[1] : ''}
  380. onChange={(value) => updateValue('default', value)}
  381. size="small"
  382. />
  383. </Card>
  384. {/* 模型专用区域 */}
  385. <div className="space-y-1">
  386. <Text strong size="small">{t('模型专用区域')}</Text>
  387. {modelEntries.map(([modelName, region], index) => (
  388. <Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
  389. <Row gutter={12} align="middle">
  390. <Col span={10}>
  391. <div className="space-y-1">
  392. <Text type="tertiary" size="small">{t('模型名称')}</Text>
  393. <Input
  394. placeholder={t('模型名称')}
  395. value={modelName}
  396. onChange={(newKey) => updateKey(modelName, newKey)}
  397. size="small"
  398. />
  399. </div>
  400. </Col>
  401. <Col span={11}>
  402. <div className="space-y-1">
  403. <Text type="tertiary" size="small">{t('区域')}</Text>
  404. <Input
  405. placeholder={t('区域')}
  406. value={region}
  407. onChange={(newValue) => updateValue(modelName, newValue)}
  408. size="small"
  409. />
  410. </div>
  411. </Col>
  412. <Col span={3}>
  413. <div className="flex justify-center pt-4">
  414. <Button
  415. icon={<IconDelete />}
  416. type="danger"
  417. theme="borderless"
  418. size="small"
  419. onClick={() => removeKeyValue(modelName)}
  420. className="hover:bg-red-50"
  421. />
  422. </div>
  423. </Col>
  424. </Row>
  425. </Card>
  426. ))}
  427. <div className="flex justify-center pt-1">
  428. <Button
  429. icon={<IconPlus />}
  430. onClick={addKeyValue}
  431. size="small"
  432. theme="solid"
  433. type="primary"
  434. className="shadow-sm hover:shadow-md transition-shadow px-4"
  435. >
  436. {t('添加模型区域')}
  437. </Button>
  438. </div>
  439. </div>
  440. </div>
  441. );
  442. };
  443. // 渲染可视化编辑器
  444. const renderVisualEditor = () => {
  445. switch (editorType) {
  446. case 'region':
  447. return renderRegionEditor();
  448. case 'object':
  449. return renderObjectEditor();
  450. case 'keyValue':
  451. default:
  452. return renderKeyValueEditor();
  453. }
  454. };
  455. const hasJsonError = jsonError && jsonError.trim() !== '';
  456. return (
  457. <div className="space-y-1">
  458. {/* Label统一显示在上方 */}
  459. {label && (
  460. <div className="flex items-center">
  461. <Text className="text-sm font-medium text-gray-900">{label}</Text>
  462. </div>
  463. )}
  464. {/* 编辑模式切换 */}
  465. <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
  466. <div className="flex items-center gap-2">
  467. {editMode === 'visual' && (
  468. <Text type="tertiary" size="small" className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs">
  469. {t('可视化模式')}
  470. </Text>
  471. )}
  472. {editMode === 'manual' && (
  473. <Text type="tertiary" size="small" className="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs">
  474. {t('手动编辑模式')}
  475. </Text>
  476. )}
  477. </div>
  478. <div className="flex items-center gap-2">
  479. {template && templateLabel && (
  480. <Button
  481. size="small"
  482. type="tertiary"
  483. onClick={fillTemplate}
  484. className="!text-semi-color-primary hover:bg-blue-50 text-xs"
  485. >
  486. {templateLabel}
  487. </Button>
  488. )}
  489. <Space size="tight">
  490. <Button
  491. size="small"
  492. type={editMode === 'visual' ? 'primary' : 'tertiary'}
  493. icon={<IconEdit />}
  494. onClick={toggleEditMode}
  495. disabled={editMode === 'manual' && hasJsonError}
  496. className={editMode === 'visual' ? 'shadow-sm' : ''}
  497. >
  498. {t('可视化')}
  499. </Button>
  500. <Button
  501. size="small"
  502. type={editMode === 'manual' ? 'primary' : 'tertiary'}
  503. icon={<IconCode />}
  504. onClick={toggleEditMode}
  505. className={editMode === 'manual' ? 'shadow-sm' : ''}
  506. >
  507. {t('手动编辑')}
  508. </Button>
  509. </Space>
  510. </div>
  511. </div>
  512. {/* JSON错误提示 */}
  513. {hasJsonError && (
  514. <Banner
  515. type="danger"
  516. description={`JSON 格式错误: ${jsonError}`}
  517. className="!rounded-md text-sm"
  518. />
  519. )}
  520. {/* 编辑器内容 */}
  521. {editMode === 'visual' ? (
  522. <div>
  523. <Card className="!p-3 !border-gray-200 !shadow-sm !rounded-md bg-white">
  524. {renderVisualEditor()}
  525. </Card>
  526. {/* 可视化模式下的额外文本显示在下方 */}
  527. {extraText && (
  528. <div className="text-xs text-gray-600 mt-0.5">
  529. {extraText}
  530. </div>
  531. )}
  532. {/* 隐藏的Form字段用于验证和数据绑定 */}
  533. <Form.Input
  534. field={field}
  535. value={value}
  536. rules={rules}
  537. style={{ display: 'none' }}
  538. noLabel={true}
  539. {...props}
  540. />
  541. </div>
  542. ) : (
  543. <Form.TextArea
  544. field={field}
  545. placeholder={placeholder}
  546. value={value}
  547. onChange={handleManualChange}
  548. showClear={showClear}
  549. rows={Math.max(8, value ? value.split('\n').length : 8)}
  550. rules={rules}
  551. noLabel={true}
  552. {...props}
  553. />
  554. )}
  555. {/* 额外文本在手动编辑模式下显示 */}
  556. {extraText && editMode === 'manual' && (
  557. <div className="text-xs text-gray-600">
  558. {extraText}
  559. </div>
  560. )}
  561. </div>
  562. );
  563. };
  564. export default JSONEditor;