From 9b73696a98e2d154a0d9759882bed00531556cdf Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Wed, 23 Jul 2025 16:49:06 +0800 Subject: [PATCH 01/20] feat: add video preview modal --- .../table/task-logs/TaskLogsColumnDefs.js | 9 ++++++++- .../components/table/task-logs/TaskLogsTable.jsx | 3 +++ web/src/components/table/task-logs/index.jsx | 9 ++++++++- .../table/task-logs/modals/ContentModal.jsx | 7 ++++++- web/src/hooks/task-logs/useTaskLogsData.js | 16 ++++++++++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/web/src/components/table/task-logs/TaskLogsColumnDefs.js b/web/src/components/table/task-logs/TaskLogsColumnDefs.js index f895bf01..d44edf05 100644 --- a/web/src/components/table/task-logs/TaskLogsColumnDefs.js +++ b/web/src/components/table/task-logs/TaskLogsColumnDefs.js @@ -211,6 +211,7 @@ export const getTaskLogsColumns = ({ copyText, openContentModal, isAdminUser, + openVideoModal, }) => { return [ { @@ -342,7 +343,13 @@ export const getTaskLogsColumns = ({ const isUrl = typeof text === 'string' && /^https?:\/\//.test(text); if (isSuccess && isVideoTask && isUrl) { return ( - + { + e.preventDefault(); + openVideoModal(text); + }} + > {t('点击预览视频')} ); diff --git a/web/src/components/table/task-logs/TaskLogsTable.jsx b/web/src/components/table/task-logs/TaskLogsTable.jsx index cacb12dd..eaf73c71 100644 --- a/web/src/components/table/task-logs/TaskLogsTable.jsx +++ b/web/src/components/table/task-logs/TaskLogsTable.jsx @@ -39,6 +39,7 @@ const TaskLogsTable = (taskLogsData) => { handlePageSizeChange, copyText, openContentModal, + openVideoModal, isAdminUser, t, COLUMN_KEYS, @@ -51,6 +52,7 @@ const TaskLogsTable = (taskLogsData) => { COLUMN_KEYS, copyText, openContentModal, + openVideoModal, isAdminUser, }); }, [ @@ -58,6 +60,7 @@ const TaskLogsTable = (taskLogsData) => { COLUMN_KEYS, copyText, openContentModal, + openVideoModal, isAdminUser, ]); diff --git a/web/src/components/table/task-logs/index.jsx b/web/src/components/table/task-logs/index.jsx index c5439bae..a12dab8a 100644 --- a/web/src/components/table/task-logs/index.jsx +++ b/web/src/components/table/task-logs/index.jsx @@ -37,7 +37,14 @@ const TaskLogsPage = () => { <> {/* Modals */} - + + {/* 新增:视频预览弹窗 */} + { return ( -

{modalContent}

+ {isVideo ? ( +
); }; diff --git a/web/src/hooks/task-logs/useTaskLogsData.js b/web/src/hooks/task-logs/useTaskLogsData.js index 70e2bf00..6f6940c4 100644 --- a/web/src/hooks/task-logs/useTaskLogsData.js +++ b/web/src/hooks/task-logs/useTaskLogsData.js @@ -63,6 +63,10 @@ export const useTaskLogsData = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [modalContent, setModalContent] = useState(''); + // 新增:视频预览弹窗状态 + const [isVideoModalOpen, setIsVideoModalOpen] = useState(false); + const [videoUrl, setVideoUrl] = useState(''); + // Form state const [formApi, setFormApi] = useState(null); let now = new Date(); @@ -243,6 +247,12 @@ export const useTaskLogsData = () => { setIsModalOpen(true); }; + // 新增:打开视频预览弹窗 + const openVideoModal = (url) => { + setVideoUrl(url); + setIsVideoModalOpen(true); + }; + // Initialize data useEffect(() => { const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE; @@ -264,6 +274,11 @@ export const useTaskLogsData = () => { setIsModalOpen, modalContent, + // 新增:视频弹窗状态 + isVideoModalOpen, + setIsVideoModalOpen, + videoUrl, + // Form state formApi, setFormApi, @@ -290,6 +305,7 @@ export const useTaskLogsData = () => { refresh, copyText, openContentModal, + openVideoModal, // 新增 enrichLogs, syncPageData, From a385c8a6f88123810ea12d441f4918db2ca05e1a Mon Sep 17 00:00:00 2001 From: simplty Date: Sun, 27 Jul 2025 16:20:48 +0800 Subject: [PATCH 02/20] =?UTF-8?q?feat(midjourney):=20=E4=B8=BA=20Midjourne?= =?UTF-8?q?y=20=E4=BB=BB=E5=8A=A1=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91=20UR?= =?UTF-8?q?L=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增了对 Midjourney 任务中 VideoUrl 和 VideoUrls 字段的映射和更新检查。 这确保了 Midjourney 生成的视频 URL 能够被正确地存储,并且任务更新能够反映这些 URL 的变化,提高了数据同步的准确性。 --- controller/midjourney.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/controller/midjourney.go b/controller/midjourney.go index 02ad708f..30a5a09a 100644 --- a/controller/midjourney.go +++ b/controller/midjourney.go @@ -145,6 +145,22 @@ func UpdateMidjourneyTaskBulk() { buttonStr, _ := json.Marshal(responseItem.Buttons) task.Buttons = string(buttonStr) } + // 映射 VideoUrl + task.VideoUrl = responseItem.VideoUrl + + // 映射 VideoUrls - 将数组序列化为 JSON 字符串 + if responseItem.VideoUrls != nil && len(responseItem.VideoUrls) > 0 { + videoUrlsStr, err := json.Marshal(responseItem.VideoUrls) + if err != nil { + common.LogError(ctx, fmt.Sprintf("序列化 VideoUrls 失败: %v", err)) + task.VideoUrls = "[]" // 失败时设置为空数组 + } else { + task.VideoUrls = string(videoUrlsStr) + } + } else { + task.VideoUrls = "" // 空值时清空字段 + } + shouldReturnQuota := false if (task.Progress != "100%" && responseItem.FailReason != "") || (task.Progress == "100%" && task.Status == "FAILURE") { common.LogInfo(ctx, task.MjId+" 构建失败,"+task.FailReason) @@ -208,6 +224,20 @@ func checkMjTaskNeedUpdate(oldTask *model.Midjourney, newTask dto.MidjourneyDto) if oldTask.Progress != "100%" && newTask.FailReason != "" { return true } + // 检查 VideoUrl 是否需要更新 + if oldTask.VideoUrl != newTask.VideoUrl { + return true + } + // 检查 VideoUrls 是否需要更新 + if newTask.VideoUrls != nil && len(newTask.VideoUrls) > 0 { + newVideoUrlsStr, _ := json.Marshal(newTask.VideoUrls) + if oldTask.VideoUrls != string(newVideoUrlsStr) { + return true + } + } else if oldTask.VideoUrls != "" { + // 如果新数据没有 VideoUrls 但旧数据有,需要更新(清空) + return true + } return false } From 1c1e3386f802e05488a480c5fb451c9437851fde Mon Sep 17 00:00:00 2001 From: ZhengJin Date: Mon, 28 Jul 2025 17:52:59 +0800 Subject: [PATCH 03/20] Update api.js --- web/src/helpers/api.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index 55228fd8..294e1775 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -215,14 +215,16 @@ export async function getOAuthState() { export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) { const state = await getOAuthState(); if (!state) return; - const redirect_uri = `${window.location.origin}/oauth/oidc`; - const response_type = 'code'; - const scope = 'openid profile email'; - const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`; + const url = new URL(auth_url); + url.searchParams.set('client_id', client_id); + url.searchParams.set('redirect_uri', `${window.location.origin}/oauth/oidc`); + url.searchParams.set('response_type', 'code'); + url.searchParams.set('scope', 'openid profile email'); + url.searchParams.set('state', state); if (openInNewTab) { - window.open(url); + window.open(url.toString(), '_blank'); } else { - window.location.href = url; + window.location.href = url.toString(); } } From 43263a3bc80fac88451f071bd652d64d0bce2f72 Mon Sep 17 00:00:00 2001 From: antecanis8 <42382878+antecanis8@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:02:57 +0000 Subject: [PATCH 04/20] fix : Gemini embedding model only embeds the first text in a batch --- dto/gemini.go | 10 +++++-- relay/channel/gemini/adaptor.go | 44 ++++++++++++++++------------ relay/channel/gemini/relay-gemini.go | 20 +++++++------ 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/dto/gemini.go b/dto/gemini.go index f7acd355..1bd1fe4c 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -216,10 +216,14 @@ type GeminiEmbeddingRequest struct { OutputDimensionality int `json:"outputDimensionality,omitempty"` } -type GeminiEmbeddingResponse struct { - Embedding ContentEmbedding `json:"embedding"` +type GeminiBatchEmbeddingRequest struct { + Requests []*GeminiEmbeddingRequest `json:"requests"` } -type ContentEmbedding struct { +type GeminiEmbedding struct { Values []float64 `json:"values"` } + +type GeminiBatchEmbeddingResponse struct { + Embeddings []*GeminiEmbedding `json:"embeddings"` +} diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index 14fd278d..efa64057 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -114,7 +114,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if strings.HasPrefix(info.UpstreamModelName, "text-embedding") || strings.HasPrefix(info.UpstreamModelName, "embedding") || strings.HasPrefix(info.UpstreamModelName, "gemini-embedding") { - return fmt.Sprintf("%s/%s/models/%s:embedContent", info.BaseUrl, version, info.UpstreamModelName), nil + return fmt.Sprintf("%s/%s/models/%s:batchEmbedContents", info.BaseUrl, version, info.UpstreamModelName), nil } action := "generateContent" @@ -156,29 +156,35 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela if len(inputs) == 0 { return nil, errors.New("input is empty") } - - // only process the first input - geminiRequest := dto.GeminiEmbeddingRequest{ - Content: dto.GeminiChatContent{ - Parts: []dto.GeminiPart{ - { - Text: inputs[0], + // process all inputs + geminiRequests := make([]map[string]interface{}, 0, len(inputs)) + for _, input := range inputs { + geminiRequest := map[string]interface{}{ + "model": fmt.Sprintf("models/%s", info.UpstreamModelName), + "content": dto.GeminiChatContent{ + Parts: []dto.GeminiPart{ + { + Text: input, + }, }, }, - }, - } - - // set specific parameters for different models - // https://ai.google.dev/api/embeddings?hl=zh-cn#method:-models.embedcontent - switch info.UpstreamModelName { - case "text-embedding-004": - // except embedding-001 supports setting `OutputDimensionality` - if request.Dimensions > 0 { - geminiRequest.OutputDimensionality = request.Dimensions } + + // set specific parameters for different models + // https://ai.google.dev/api/embeddings?hl=zh-cn#method:-models.embedcontent + switch info.UpstreamModelName { + case "text-embedding-004": + // except embedding-001 supports setting `OutputDimensionality` + if request.Dimensions > 0 { + geminiRequest["outputDimensionality"] = request.Dimensions + } + } + geminiRequests = append(geminiRequests, geminiRequest) } - return geminiRequest, nil + return map[string]interface{}{ + "requests": geminiRequests, + }, nil } func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index adc771e2..0b6e63a6 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -974,7 +974,7 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h return nil, types.NewOpenAIError(readErr, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - var geminiResponse dto.GeminiEmbeddingResponse + var geminiResponse dto.GeminiBatchEmbeddingResponse if jsonErr := common.Unmarshal(responseBody, &geminiResponse); jsonErr != nil { return nil, types.NewOpenAIError(jsonErr, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } @@ -982,14 +982,16 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *h // convert to openai format response openAIResponse := dto.OpenAIEmbeddingResponse{ Object: "list", - Data: []dto.OpenAIEmbeddingResponseItem{ - { - Object: "embedding", - Embedding: geminiResponse.Embedding.Values, - Index: 0, - }, - }, - Model: info.UpstreamModelName, + Data: make([]dto.OpenAIEmbeddingResponseItem, 0, len(geminiResponse.Embeddings)), + Model: info.UpstreamModelName, + } + + for i, embedding := range geminiResponse.Embeddings { + openAIResponse.Data = append(openAIResponse.Data, dto.OpenAIEmbeddingResponseItem{ + Object: "embedding", + Embedding: embedding.Values, + Index: i, + }) } // calculate usage From 49abd6aaf31aaea1274b9b7ba3a773ed72ca6506 Mon Sep 17 00:00:00 2001 From: antecanis8 <42382878+antecanis8@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:19:19 +0000 Subject: [PATCH 05/20] feat: add support for configuring output dimensionality for multiple Gemini new models --- relay/channel/gemini/adaptor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index efa64057..0f561023 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -173,8 +173,8 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela // set specific parameters for different models // https://ai.google.dev/api/embeddings?hl=zh-cn#method:-models.embedcontent switch info.UpstreamModelName { - case "text-embedding-004": - // except embedding-001 supports setting `OutputDimensionality` + case "text-embedding-004","gemini-embedding-exp-03-07","gemini-embedding-001": + // Only newer models introduced after 2024 support OutputDimensionality if request.Dimensions > 0 { geminiRequest["outputDimensionality"] = request.Dimensions } From 2471367c9205720cc985aa1578ce63a290508a74 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:00:02 +0800 Subject: [PATCH 06/20] feat: optimized Json Visual Editor(JSONEditor) when detected duplicate key --- web/src/components/common/ui/JSONEditor.js | 641 ++++++++++----------- 1 file changed, 313 insertions(+), 328 deletions(-) diff --git a/web/src/components/common/ui/JSONEditor.js b/web/src/components/common/ui/JSONEditor.js index f4f5eee9..5d4d4d32 100644 --- a/web/src/components/common/ui/JSONEditor.js +++ b/web/src/components/common/ui/JSONEditor.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, @@ -15,12 +15,14 @@ import { Row, Col, Divider, + Tooltip, } from '@douyinfe/semi-ui'; import { IconCode, IconPlus, IconDelete, IconRefresh, + IconAlertTriangle, } from '@douyinfe/semi-icons'; const { Text } = Typography; @@ -43,24 +45,44 @@ const JSONEditor = ({ }) => { const { t } = useTranslation(); - // 初始化JSON数据 - const [jsonData, setJsonData] = useState(() => { - // 初始化时解析JSON数据 + // 将对象转换为键值对数组(包含唯一ID) + const objectToKeyValueArray = useCallback((obj) => { + if (!obj || typeof obj !== 'object') return []; + return Object.entries(obj).map(([key, value], index) => ({ + id: `${Date.now()}_${index}_${Math.random()}`, // 唯一ID + key, + value + })); + }, []); + + // 将键值对数组转换为对象(重复键时后面的会覆盖前面的) + const keyValueArrayToObject = useCallback((arr) => { + const result = {}; + arr.forEach(item => { + if (item.key) { + result[item.key] = item.value; + } + }); + return result; + }, []); + + // 初始化键值对数组 + const [keyValuePairs, setKeyValuePairs] = useState(() => { if (typeof value === 'string' && value.trim()) { try { const parsed = JSON.parse(value); - return parsed; + return objectToKeyValueArray(parsed); } catch (error) { - return {}; + return []; } } if (typeof value === 'object' && value !== null) { - return value; + return objectToKeyValueArray(value); } - return {}; + return []; }); - // 手动模式下的本地文本缓冲,避免无效 JSON 时被外部值重置 + // 手动模式下的本地文本缓冲 const [manualText, setManualText] = useState(() => { if (typeof value === 'string') return value; if (value && typeof value === 'object') return JSON.stringify(value, null, 2); @@ -69,22 +91,38 @@ const JSONEditor = ({ // 根据键数量决定默认编辑模式 const [editMode, setEditMode] = useState(() => { - // 如果初始JSON数据的键数量大于10个,则默认使用手动模式 if (typeof value === 'string' && value.trim()) { try { const parsed = JSON.parse(value); const keyCount = Object.keys(parsed).length; return keyCount > 10 ? 'manual' : 'visual'; } catch (error) { - // JSON无效时默认显示手动编辑模式 return 'manual'; } } return 'visual'; }); + const [jsonError, setJsonError] = useState(''); - // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效) + // 计算重复的键 + const duplicateKeys = useMemo(() => { + const keyCount = {}; + const duplicates = new Set(); + + keyValuePairs.forEach(pair => { + if (pair.key) { + keyCount[pair.key] = (keyCount[pair.key] || 0) + 1; + if (keyCount[pair.key] > 1) { + duplicates.add(pair.key); + } + } + }); + + return duplicates; + }, [keyValuePairs]); + + // 数据同步 - 当value变化时更新键值对数组 useEffect(() => { try { let parsed = {}; @@ -93,16 +131,20 @@ const JSONEditor = ({ } else if (typeof value === 'object' && value !== null) { parsed = value; } - setJsonData(parsed); + + // 只在外部值真正改变时更新,避免循环更新 + const currentObj = keyValueArrayToObject(keyValuePairs); + if (JSON.stringify(parsed) !== JSON.stringify(currentObj)) { + setKeyValuePairs(objectToKeyValueArray(parsed)); + } setJsonError(''); } catch (error) { console.log('JSON解析失败:', error.message); setJsonError(error.message); - // JSON格式错误时不更新jsonData } }, [value]); - // 外部 value 变化时,若不在手动模式,则同步手动文本;在手动模式下不打断用户输入 + // 外部 value 变化时,若不在手动模式,则同步手动文本 useEffect(() => { if (editMode !== 'manual') { if (typeof value === 'string') setManualText(value); @@ -112,45 +154,47 @@ const JSONEditor = ({ }, [value, editMode]); // 处理可视化编辑的数据变化 - const handleVisualChange = useCallback((newData) => { - setJsonData(newData); + const handleVisualChange = useCallback((newPairs) => { + setKeyValuePairs(newPairs); + const jsonObject = keyValueArrayToObject(newPairs); + const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2); + setJsonError(''); - const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2); - // 通过formApi设置值(如果提供的话) + // 通过formApi设置值 if (formApi && field) { formApi.setValue(field, jsonString); } onChange?.(jsonString); - }, [onChange, formApi, field]); + }, [onChange, formApi, field, keyValueArrayToObject]); - // 处理手动编辑的数据变化(无效 JSON 不阻断输入,也不立刻回传上游) + // 处理手动编辑的数据变化 const handleManualChange = useCallback((newValue) => { setManualText(newValue); if (newValue && newValue.trim()) { try { - JSON.parse(newValue); + const parsed = JSON.parse(newValue); + setKeyValuePairs(objectToKeyValueArray(parsed)); setJsonError(''); onChange?.(newValue); } catch (error) { setJsonError(error.message); - // 无效 JSON 时不回传,避免外部值把输入重置 } } else { + setKeyValuePairs([]); setJsonError(''); onChange?.(''); } - }, [onChange]); + }, [onChange, objectToKeyValueArray]); // 切换编辑模式 const toggleEditMode = useCallback(() => { if (editMode === 'visual') { - // 从可视化模式切换到手动模式 - setManualText(Object.keys(jsonData).length === 0 ? '' : JSON.stringify(jsonData, null, 2)); + const jsonObject = keyValueArrayToObject(keyValuePairs); + setManualText(Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2)); setEditMode('manual'); } else { - // 从手动模式切换到可视化模式,需要验证JSON try { let parsed = {}; if (manualText && manualText.trim()) { @@ -160,98 +204,166 @@ const JSONEditor = ({ } else if (typeof value === 'object' && value !== null) { parsed = value; } - setJsonData(parsed); + setKeyValuePairs(objectToKeyValueArray(parsed)); setJsonError(''); setEditMode('visual'); } catch (error) { setJsonError(error.message); - // JSON格式错误时不切换模式 return; } } - }, [editMode, value, manualText, jsonData]); + }, [editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray]); // 添加键值对 const addKeyValue = useCallback(() => { - const newData = { ...jsonData }; - const keys = Object.keys(newData); + const newPairs = [...keyValuePairs]; + const existingKeys = newPairs.map(p => p.key); let counter = 1; let newKey = `field_${counter}`; - while (newData.hasOwnProperty(newKey)) { + while (existingKeys.includes(newKey)) { counter += 1; newKey = `field_${counter}`; } - newData[newKey] = ''; - handleVisualChange(newData); - }, [jsonData, handleVisualChange]); + newPairs.push({ + id: `${Date.now()}_${Math.random()}`, + key: newKey, + value: '' + }); + handleVisualChange(newPairs); + }, [keyValuePairs, handleVisualChange]); // 删除键值对 - const removeKeyValue = useCallback((keyToRemove) => { - const newData = { ...jsonData }; - delete newData[keyToRemove]; - handleVisualChange(newData); - }, [jsonData, handleVisualChange]); + const removeKeyValue = useCallback((id) => { + const newPairs = keyValuePairs.filter(pair => pair.id !== id); + handleVisualChange(newPairs); + }, [keyValuePairs, handleVisualChange]); // 更新键名 - const updateKey = useCallback((oldKey, newKey) => { - if (oldKey === newKey || !newKey) return; - const newData = {}; - Object.entries(jsonData).forEach(([k, v]) => { - if (k === oldKey) { - newData[newKey] = v; - } else { - newData[k] = v; - } - }); - handleVisualChange(newData); - }, [jsonData, handleVisualChange]); + const updateKey = useCallback((id, newKey) => { + const newPairs = keyValuePairs.map(pair => + pair.id === id ? { ...pair, key: newKey } : pair + ); + handleVisualChange(newPairs); + }, [keyValuePairs, handleVisualChange]); // 更新值 - const updateValue = useCallback((key, newValue) => { - const newData = { ...jsonData }; - newData[key] = newValue; - handleVisualChange(newData); - }, [jsonData, handleVisualChange]); + const updateValue = useCallback((id, newValue) => { + const newPairs = keyValuePairs.map(pair => + pair.id === id ? { ...pair, value: newValue } : pair + ); + handleVisualChange(newPairs); + }, [keyValuePairs, handleVisualChange]); // 填入模板 const fillTemplate = useCallback(() => { if (template) { const templateString = JSON.stringify(template, null, 2); - // 通过formApi设置值(如果提供的话) if (formApi && field) { formApi.setValue(field, templateString); } - // 同步内部与外部值,避免出现杂字符 setManualText(templateString); - setJsonData(template); + setKeyValuePairs(objectToKeyValueArray(template)); onChange?.(templateString); - - // 清除错误状态 setJsonError(''); } - }, [template, onChange, editMode, formApi, field]); + }, [template, onChange, formApi, field, objectToKeyValueArray]); - // 渲染键值对编辑器 - const renderKeyValueEditor = () => { - if (typeof jsonData !== 'object' || jsonData === null) { + // 渲染值输入控件(支持嵌套) + const renderValueInput = (pairId, value) => { + const valueType = typeof value; + + if (valueType === 'boolean') { return ( -
-
- -
- - {t('无效的JSON数据,请检查格式')} +
+ updateValue(pairId, newValue)} + /> + + {value ? t('true') : t('false')}
); } - const entries = Object.entries(jsonData); + if (valueType === 'number') { + return ( + updateValue(pairId, newValue)} + style={{ width: '100%' }} + placeholder={t('输入数字')} + /> + ); + } + + if (valueType === 'object' && value !== null) { + // 简化嵌套对象的处理,使用TextArea + return ( +