feat: 支持在 WebUI 自定义 OpenAI API extra_body 参数 (#2719)

* feat: 支持OPENAI系 模型的自定义标头,以解决qwen模型无法使用的问题

* fix: 修复AI说的问题

* fix: 布尔开关向右对齐
This commit is contained in:
Yokami
2025-09-13 13:23:49 +08:00
committed by GitHub
parent ea6f209557
commit e841b6af88
5 changed files with 388 additions and 63 deletions
+20
View File
@@ -599,6 +599,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.openai.com/v1",
"timeout": 120,
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
"hint": "也兼容所有与 OpenAI API 兼容的服务。",
},
@@ -613,6 +614,7 @@ CONFIG_METADATA_2 = {
"api_base": "",
"timeout": 120,
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"xAI": {
@@ -625,6 +627,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.x.ai/v1",
"timeout": 120,
"model_config": {"model": "grok-2-latest", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"Anthropic": {
@@ -654,6 +657,7 @@ CONFIG_METADATA_2 = {
"key": ["ollama"], # ollama 的 key 默认是 ollama
"api_base": "http://localhost:11434/v1",
"model_config": {"model": "llama3.1-8b", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"LM Studio": {
@@ -667,6 +671,7 @@ CONFIG_METADATA_2 = {
"model_config": {
"model": "llama-3.1-8b",
},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"Gemini(OpenAI兼容)": {
@@ -682,6 +687,7 @@ CONFIG_METADATA_2 = {
"model": "gemini-1.5-flash",
"temperature": 0.4,
},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"Gemini": {
@@ -722,6 +728,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.deepseek.com/v1",
"timeout": 120,
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"302.AI": {
@@ -734,6 +741,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.302.ai/v1",
"timeout": 120,
"model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"硅基流动": {
@@ -749,6 +757,7 @@ CONFIG_METADATA_2 = {
"model": "deepseek-ai/DeepSeek-V3",
"temperature": 0.4,
},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"PPIO派欧云": {
@@ -764,6 +773,7 @@ CONFIG_METADATA_2 = {
"model": "deepseek/deepseek-r1",
"temperature": 0.4,
},
"custom_extra_body": {},
},
"优云智算": {
"id": "compshare",
@@ -777,6 +787,7 @@ CONFIG_METADATA_2 = {
"model_config": {
"model": "moonshotai/Kimi-K2-Instruct",
},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"Kimi": {
@@ -789,6 +800,7 @@ CONFIG_METADATA_2 = {
"timeout": 120,
"api_base": "https://api.moonshot.cn/v1",
"model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"智谱 AI": {
@@ -847,6 +859,7 @@ CONFIG_METADATA_2 = {
"timeout": 120,
"api_base": "https://api-inference.modelscope.cn/v1",
"model_config": {"model": "Qwen/Qwen3-32B", "temperature": 0.4},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"FastGPT": {
@@ -858,6 +871,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.fastgpt.in/api/v1",
"timeout": 60,
"custom_extra_body": {},
},
"Whisper(API)": {
"id": "whisper",
@@ -1102,6 +1116,12 @@ CONFIG_METADATA_2 = {
"render_type": "checkbox",
"hint": "模型支持的模态。如所填写的模型不支持图像,请取消勾选图像。",
},
"custom_extra_body": {
"description": "自定义请求体参数",
"type": "dict",
"items": {},
"hint": "此处添加的键值对将被合并到发送给 API 的 extra_body 中。值可以是字符串、数字或布尔值。",
},
"provider": {
"type": "string",
"invisible": True,
+12 -5
View File
@@ -99,12 +99,13 @@ class ProviderOpenAIOfficial(Provider):
for key in to_del:
del payloads[key]
model = payloads.get("model", "")
# 针对 qwen3 非 thinking 模型的特殊处理:非流式调用必须设置 enable_thinking=false
if "qwen3" in model.lower() and "thinking" not in model.lower():
extra_body["enable_thinking"] = False
# 读取并合并 custom_extra_body 配置
custom_extra_body = self.provider_config.get("custom_extra_body", {})
if isinstance(custom_extra_body, dict):
extra_body.update(custom_extra_body)
# 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
elif model == "deepseek-reasoner" and "tools" in payloads:
if model == "deepseek-reasoner" and "tools" in payloads:
del payloads["tools"]
completion = await self.client.chat.completions.create(
@@ -137,6 +138,12 @@ class ProviderOpenAIOfficial(Provider):
# 不在默认参数中的参数放在 extra_body 中
extra_body = {}
# 读取并合并 custom_extra_body 配置
custom_extra_body = self.provider_config.get("custom_extra_body", {})
if isinstance(custom_extra_body, dict):
extra_body.update(custom_extra_body)
to_del = []
for key in payloads.keys():
if key not in self.default_params:
@@ -2,6 +2,7 @@
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { ref, computed } from 'vue'
import ListConfigItem from './ListConfigItem.vue'
import ObjectEditor from './ObjectEditor.vue'
import ProviderSelector from './ProviderSelector.vue'
import PersonaSelector from './PersonaSelector.vue'
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
@@ -80,7 +81,7 @@ function shouldShowItem(itemMeta, itemKey) {
function hasVisibleItemsAfter(items, currentIndex) {
const itemEntries = Object.entries(items)
// 检查当前索引之后是否还有可见的配置项
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
const [itemKey, itemValue] = itemEntries[i]
@@ -89,7 +90,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
return true
}
}
return false
}
</script>
@@ -130,7 +131,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-expand-transition>
</div>
</div>
<!-- Regular Property -->
<template v-else>
<v-row v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="config-row">
@@ -145,7 +146,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-list-item-title>
<v-list-item-subtitle class="property-hint">
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
class="important-hint"></span>
{{ metadata[metadataKey].items[key]?.hint }}
</v-list-item-subtitle>
@@ -153,10 +154,10 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-col>
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
color="primary"
label
size="x-small"
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
color="primary"
label
size="x-small"
variant="flat">
{{ metadata[metadataKey].items[key]?.type || 'string' }}
</v-chip>
@@ -166,35 +167,35 @@ function hasVisibleItemsAfter(items, currentIndex) {
<div v-if="metadata[metadataKey].items[key]" class="w-100">
<!-- Special handling for specific metadata types -->
<div v-if="metadata[metadataKey].items[key]?._special === 'select_provider'">
<ProviderSelector
<ProviderSelector
v-model="iterable[key]"
:provider-type="'chat_completion'"
/>
</div>
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_provider_stt'">
<ProviderSelector
<ProviderSelector
v-model="iterable[key]"
:provider-type="'speech_to_text'"
/>
</div>
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_provider_tts'">
<ProviderSelector
<ProviderSelector
v-model="iterable[key]"
:provider-type="'text_to_speech'"
/>
</div>
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_persona'">
<PersonaSelector
<PersonaSelector
v-model="iterable[key]"
/>
</div>
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_knowledgebase'">
<KnowledgeBaseSelector
<KnowledgeBaseSelector
v-model="iterable[key]"
/>
</div>
<!-- List item with options-->
<div v-else-if="metadata[metadataKey].items[key]?.type === 'list' && metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible && metadata[metadataKey].items[key]?.render_type === 'checkbox'"
<div v-else-if="metadata[metadataKey].items[key]?.type === 'list' && metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible && metadata[metadataKey].items[key]?.render_type === 'checkbox'"
class="d-flex flex-wrap gap-20">
<v-checkbox
v-for="(option, index) in metadata[metadataKey].items[key]?.options"
@@ -233,10 +234,10 @@ function hasVisibleItemsAfter(items, currentIndex) {
<!-- Code Editor with Full Screen Option -->
<div v-else-if="metadata[metadataKey].items[key]?.editor_mode && !metadata[metadataKey].items[key]?.invisible" class="editor-container">
<VueMonacoEditor
:theme="metadata[metadataKey].items[key]?.editor_theme || 'vs-light'"
<VueMonacoEditor
:theme="metadata[metadataKey].items[key]?.editor_theme || 'vs-light'"
:language="metadata[metadataKey].items[key]?.editor_language || 'json'"
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
v-model:value="iterable[key]"
>
</VueMonacoEditor>
@@ -252,7 +253,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
<v-icon>mdi-fullscreen</v-icon>
</v-btn>
</div>
<!-- String input -->
<v-text-field
v-else-if="metadata[metadataKey].items[key]?.type === 'string' && !metadata[metadataKey].items[key]?.invisible"
@@ -262,7 +263,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
class="config-field"
hide-details
></v-text-field>
<!-- Numeric input -->
<v-text-field
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey]?.invisible"
@@ -273,7 +274,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
type="number"
hide-details
></v-text-field>
<!-- Text area -->
<v-textarea
v-else-if="metadata[metadataKey].items[key]?.type === 'text' && !metadata[metadataKey].items[key]?.invisible"
@@ -283,7 +284,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
class="config-field"
hide-details
></v-textarea>
<!-- Boolean switch -->
<v-switch
v-else-if="metadata[metadataKey].items[key]?.type === 'bool' && !metadata[metadataKey].items[key]?.invisible"
@@ -293,20 +294,27 @@ function hasVisibleItemsAfter(items, currentIndex) {
density="compact"
hide-details
></v-switch>
<!-- List item -->
<ListConfigItem
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
v-model="iterable[key]"
class="config-field"
/>
<!-- Dict item (key-value editor) -->
<ObjectEditor
v-else-if="metadata[metadataKey].items[key]?.type === 'dict' && !metadata[metadataKey].items[key]?.invisible"
v-model="iterable[key]"
class="config-field"
/>
</div>
<!-- Fallback for unknown metadata -->
<div v-else class="w-100">
<v-text-field
v-model="iterable[key]"
:label="key"
<v-text-field
v-model="iterable[key]"
:label="key"
density="compact"
variant="outlined"
class="config-field"
@@ -316,14 +324,14 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-col>
</v-row>
<v-divider
<v-divider
v-if="hasVisibleItemsAfter(filteredIterable, index) && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
class="config-divider"
></v-divider>
</template>
</div>
</div>
<!-- Simple Value Configuration -->
<div v-else class="simple-config">
<v-row class="config-row">
@@ -342,9 +350,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-col>
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
<v-chip v-if="!metadata[metadataKey]?.invisible"
color="primary"
label
<v-chip v-if="!metadata[metadataKey]?.invisible"
color="primary"
label
size="x-small"
variant="flat">
{{ metadata[metadataKey]?.type }}
@@ -364,7 +372,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
class="config-field"
hide-details
></v-select>
<!-- String input -->
<v-text-field
v-else-if="metadata[metadataKey]?.type === 'string' && !metadata[metadataKey]?.invisible"
@@ -374,7 +382,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
class="config-field"
hide-details
></v-text-field>
<!-- Numeric input -->
<v-text-field
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
@@ -385,7 +393,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
type="number"
hide-details
></v-text-field>
<!-- Text area -->
<v-textarea
v-else-if="metadata[metadataKey]?.type === 'text' && !metadata[metadataKey]?.invisible"
@@ -396,7 +404,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
class="config-field"
hide-details
></v-textarea>
<!-- Boolean switch -->
<v-switch
v-else-if="metadata[metadataKey]?.type === 'bool' && !metadata[metadataKey]?.invisible"
@@ -406,9 +414,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
density="compact"
hide-details
></v-switch>
<!-- List item -->
<ListConfigItem
<ListConfigItem
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
v-model="iterable[metadataKey]"
class="config-field"
@@ -435,9 +443,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-toolbar-items>
</v-toolbar>
<v-card-text class="pa-0">
<VueMonacoEditor
<VueMonacoEditor
:theme="currentEditingTheme"
:language="currentEditingLanguage"
:language="currentEditingLanguage"
style="height: calc(100vh - 64px);"
v-model:value="currentEditingKeyIterable[currentEditingKey]"
>
@@ -567,11 +575,11 @@ function hasVisibleItemsAfter(items, currentIndex) {
.nested-object {
padding-left: 8px;
}
.config-row {
padding: 8px 0;
}
.property-info, .type-indicator, .config-input {
padding: 4px;
}
@@ -2,6 +2,7 @@
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { ref, computed } from 'vue'
import ListConfigItem from './ListConfigItem.vue'
import ObjectEditor from './ObjectEditor.vue'
import ProviderSelector from './ProviderSelector.vue'
import PersonaSelector from './PersonaSelector.vue'
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
@@ -102,7 +103,7 @@ function shouldShowItem(itemMeta, itemKey) {
function hasVisibleItemsAfter(items, currentIndex) {
const itemEntries = Object.entries(items)
// 检查当前索引之后是否还有可见的配置项
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
const [itemKey, itemMeta] = itemEntries[i]
@@ -110,7 +111,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
return true
}
}
return false
}
</script>
@@ -188,13 +189,20 @@ function hasVisibleItemsAfter(items, currentIndex) {
color="primary" inset density="compact" hide-details style="display: flex; justify-content: end;"></v-switch>
<!-- List item for JSON selector -->
<ListConfigItem
<ListConfigItem
v-else-if="itemMeta?.type === 'list'"
v-model="createSelectorModel(itemKey).value"
button-text="修改"
class="config-field"
/>
<!-- Object editor for JSON selector -->
<ObjectEditor
v-else-if="itemMeta?.type === 'dict'"
v-model="createSelectorModel(itemKey).value"
class="config-field"
/>
<!-- Fallback for JSON selector -->
<v-text-field v-else v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined"
class="config-field" hide-details></v-text-field>
@@ -202,48 +210,48 @@ function hasVisibleItemsAfter(items, currentIndex) {
<!-- Special handling for specific metadata types -->
<div v-else-if="itemMeta?._special === 'select_provider'">
<ProviderSelector
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'chat_completion'"
/>
</div>
<div v-else-if="itemMeta?._special === 'select_provider_stt'">
<ProviderSelector
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'speech_to_text'"
/>
</div>
<div v-else-if="itemMeta?._special === 'select_provider_tts'">
<ProviderSelector
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'text_to_speech'"
/>
</div>
<div v-else-if="itemMeta?._special === 'provider_pool'">
<ProviderSelector
<ProviderSelector
v-model="createSelectorModel(itemKey).value"
:provider-type="'chat_completion'"
button-text="选择提供商池..."
/>
</div>
<div v-else-if="itemMeta?._special === 'select_persona'">
<PersonaSelector
<PersonaSelector
v-model="createSelectorModel(itemKey).value"
/>
</div>
<div v-else-if="itemMeta?._special === 'persona_pool'">
<PersonaSelector
<PersonaSelector
v-model="createSelectorModel(itemKey).value"
button-text="选择人格池..."
/>
</div>
<div v-else-if="itemMeta?._special === 'select_knowledgebase'">
<KnowledgeBaseSelector
<KnowledgeBaseSelector
v-model="createSelectorModel(itemKey).value"
/>
</div>
<div v-else-if="itemMeta?._special === 'select_plugin_set'">
<PluginSetSelector
<PluginSetSelector
v-model="createSelectorModel(itemKey).value"
/>
</div>
@@ -261,12 +269,12 @@ function hasVisibleItemsAfter(items, currentIndex) {
<small class="text-grey">已选择的插件</small>
</div>
<div class="d-flex flex-wrap ga-2 mt-2">
<v-chip
v-for="plugin in (createSelectorModel(itemKey).value || [])"
:key="plugin"
size="small"
label
color="primary"
<v-chip
v-for="plugin in (createSelectorModel(itemKey).value || [])"
:key="plugin"
size="small"
label
color="primary"
variant="outlined"
>
{{ plugin === '*' ? '所有插件' : plugin }}
@@ -0,0 +1,282 @@
<template>
<div class="d-flex align-center justify-space-between">
<div>
<span v-if="!modelValue || Object.keys(modelValue).length === 0" style="color: rgb(var(--v-theme-primaryText));">
暂无项目
</span>
<div v-else class="d-flex flex-wrap ga-2">
<v-chip v-for="key in displayKeys" :key="key" size="x-small" label color="primary">
{{ key.length > 20 ? key.slice(0, 20) + '...' : key }}
</v-chip>
<v-chip v-if="Object.keys(modelValue).length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
+{{ Object.keys(modelValue).length - maxDisplayItems }}
</v-chip>
</div>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
</v-btn>
</div>
<!-- Key-Value Management Dialog -->
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
{{ dialogTitle }}
</v-card-title>
<v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;">
<div v-if="localKeyValuePairs.length > 0">
<div v-for="(pair, index) in localKeyValuePairs" :key="index" class="key-value-pair">
<v-row no-gutters align="center" class="mb-2">
<v-col cols="4">
<v-text-field
v-model="pair.key"
density="compact"
variant="outlined"
hide-details
placeholder="键名"
@blur="updateKey(index, pair.key)"
></v-text-field>
</v-col>
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
<v-text-field
v-if="pair.type === 'string'"
v-model="pair.value"
density="compact"
variant="outlined"
hide-details
placeholder="字符串值"
></v-text-field>
<v-text-field
v-else-if="pair.type === 'number'"
v-model.number="pair.value"
type="number"
density="compact"
variant="outlined"
hide-details
placeholder="数值"
></v-text-field>
<v-switch
v-else-if="pair.type === 'boolean'"
v-model="pair.value"
density="compact"
hide-details
color="primary"
></v-switch>
</v-col>
<v-col cols="1" class="pl-2">
<v-btn
icon
variant="text"
size="small"
color="error"
@click="removeKeyValuePair(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
</div>
<div v-else class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon>
<p class="text-grey mt-4">暂无参数</p>
</div>
</v-card-text>
<!-- Add new key-value pair section -->
<v-card-text class="pa-4">
<div class="d-flex align-center ga-2">
<v-text-field
v-model="newKey"
label="新键名"
density="compact"
variant="outlined"
hide-details
class="flex-grow-1"
></v-text-field>
<v-select
v-model="newValueType"
:items="['string', 'number', 'boolean']"
label="值类型"
density="compact"
variant="outlined"
hide-details
style="max-width: 120px;"
></v-select>
<v-btn @click="addKeyValuePair" variant="tonal" color="primary">
<v-icon>mdi-plus</v-icon>
添加
</v-btn>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelDialog">取消</v-btn>
<v-btn color="primary" @click="confirmDialog">确认</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useI18n } from '@/i18n/composables'
const { t } = useI18n()
const props = defineProps({
modelValue: {
type: Object,
required: true
},
buttonText: {
type: String,
default: '修改'
},
dialogTitle: {
type: String,
default: '修改键值对'
},
maxDisplayItems: {
type: Number,
default: 1
}
})
const emit = defineEmits(['update:modelValue'])
const dialog = ref(false)
const localKeyValuePairs = ref([])
const originalKeyValuePairs = ref([])
const newKey = ref('')
const newValueType = ref('string')
// 计算要显示的键名
const displayKeys = computed(() => {
return Object.keys(props.modelValue).slice(0, props.maxDisplayItems)
})
// 监听 modelValue 变化,主要用于初始化
watch(() => props.modelValue, (newValue) => {
// This watch is primarily for initialization or external changes
// The dialog-based editing handles internal updates
}, { immediate: true })
function initializeLocalKeyValuePairs() {
localKeyValuePairs.value = []
for (const [key, value] of Object.entries(props.modelValue)) {
localKeyValuePairs.value.push({
key: key,
value: value,
type: typeof value // Store the original type
})
}
}
function openDialog() {
initializeLocalKeyValuePairs()
originalKeyValuePairs.value = JSON.parse(JSON.stringify(localKeyValuePairs.value)) // Deep copy
newKey.value = ''
newValueType.value = 'string'
dialog.value = true
}
function addKeyValuePair() {
const key = newKey.value.trim()
if (key !== '') {
const isKeyExists = localKeyValuePairs.value.some(pair => pair.key === key)
if (isKeyExists) {
alert('键名已存在')
return
}
let defaultValue
switch (newValueType.value) {
case 'number':
defaultValue = 0
break
case 'boolean':
defaultValue = false
break
default: // string
defaultValue = ""
break
}
localKeyValuePairs.value.push({
key: key,
value: defaultValue,
type: newValueType.value
})
newKey.value = ''
}
}
function removeKeyValuePair(index) {
localKeyValuePairs.value.splice(index, 1)
}
function updateKey(index, newKey) {
const originalKey = localKeyValuePairs.value[index].key
// 如果键名没有改变,则不执行任何操作
if (originalKey === newKey) return
// 检查新键名是否已存在
const isKeyExists = localKeyValuePairs.value.some((pair, i) => i !== index && pair.key === newKey)
if (isKeyExists) {
// 如果键名已存在,提示用户并恢复原值
alert('键名已存在')
// 将键名恢复为修改前的原始值
localKeyValuePairs.value[index].key = originalKey
return
}
// 更新本地副本
localKeyValuePairs.value[index].key = newKey
}
function confirmDialog() {
const updatedValue = {}
for (const pair of localKeyValuePairs.value) {
let convertedValue = pair.value
// 根据声明的类型进行转换
switch (pair.type) {
case 'number':
// 尝试转换为数字,如果失败则保持原值(或设为默认值0)
convertedValue = Number(pair.value)
// 可选:检查是否为有效数字,无效则设为0或报错
// if (isNaN(convertedValue)) convertedValue = 0;
break
case 'boolean':
// 布尔值通常由 v-switch 正确处理,但为保险起见可以显式转换
// 注意:在 JavaScript 中,只有严格的 false, 0, "", null, undefined, NaN 会被转换为 false
// 这里直接赋值 pair.value 应该是安全的,因为 v-model 绑定的就是布尔值
// convertedValue = Boolean(pair.value)
break
case 'string':
default:
// 默认转换为字符串
convertedValue = String(pair.value)
break
}
updatedValue[pair.key] = convertedValue
}
emit('update:modelValue', updatedValue)
dialog.value = false
}
function cancelDialog() {
// Reset to original state
localKeyValuePairs.value = JSON.parse(JSON.stringify(originalKeyValuePairs.value))
dialog.value = false
}
</script>
<style scoped>
.key-value-pair {
width: 100%;
}
</style>