astrbbbb / dashboard /src /components /shared /ObjectEditor.vue
qa1145's picture
Upload 1245 files
8ede856 verified
<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));">
{{ t('core.common.objectEditor.noItems') }}
</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">
{{ resolveButtonText }}
</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;">
{{ resolveDialogTitle }}
</v-card-title>
<v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;">
<!-- Regular key-value pairs (non-template) -->
<div v-if="nonTemplatePairs.length > 0">
<div v-for="(pair, index) in nonTemplatePairs" :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="t('core.common.objectEditor.placeholders.keyName')"
@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="t('core.common.objectEditor.placeholders.stringValue')"
></v-text-field>
<div v-else-if="pair.type === 'number' || pair.type === 'float' || pair.type === 'int'" class="d-flex align-center gap-2 flex-grow-1">
<v-slider
v-if="pair.slider"
:model-value="Number(pair.value) || 0"
@update:model-value="pair.value = $event"
:min="pair.slider.min"
:max="pair.slider.max"
:step="pair.slider.step"
color="primary"
density="compact"
hide-details
class="flex-grow-1"
></v-slider>
<v-text-field
v-model.number="pair.value"
type="number"
density="compact"
variant="outlined"
hide-details
:placeholder="t('core.common.objectEditor.placeholders.numberValue')"
:style="pair.slider ? 'max-width: 120px;' : ''"
></v-text-field>
</div>
<v-switch
v-else-if="pair.type === 'boolean'"
v-model="pair.value"
density="compact"
hide-details
color="primary"
></v-switch>
<v-text-field
v-if="pair.type === 'json'"
v-model="pair.value"
density="compact"
variant="outlined"
hide-details="auto"
:placeholder="t('core.common.objectEditor.placeholders.jsonValue')"
@blur="updateJSON(index, pair.value)"
:error-messages="pair.jsonError"
></v-text-field>
</v-col>
<v-col cols="1" class="pl-2">
<v-btn
icon
variant="text"
size="small"
color="error"
@click="removeKeyValuePairByKey(pair.key)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
</div>
<!-- Template schema fields -->
<div v-if="hasTemplateSchema" class="mt-4">
<v-divider class="mb-3"></v-divider>
<div class="text-caption text-grey mb-2">{{ t('core.common.objectEditor.presets') }}</div>
<div v-for="(template, templateKey) in templateSchema" :key="templateKey" class="template-field" :class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }">
<v-row no-gutters align="center" class="mb-2">
<v-col cols="4">
<div class="d-flex flex-column">
<span class="text-caption font-weight-medium">{{ getTemplateTitle(template, templateKey) }}</span>
<span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ translateIfKey(template.hint) }}</span>
</div>
</v-col>
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
<v-text-field
v-if="template.type === 'string'"
:model-value="getTemplateValue(templateKey)"
@update:model-value="updateTemplateValue(templateKey, $event)"
density="compact"
variant="outlined"
hide-details
:placeholder="t('core.common.objectEditor.placeholders.stringValue')"
></v-text-field>
<div v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'" class="d-flex align-center ga-4 flex-grow-1">
<v-slider
v-if="template.slider"
:model-value="Number(getTemplateValue(templateKey)) || 0"
@update:model-value="updateTemplateValue(templateKey, $event)"
:min="template.slider.min"
:max="template.slider.max"
:step="template.slider.step"
color="primary"
density="compact"
hide-details
class="flex-grow-1"
></v-slider>
<v-text-field
:model-value="getTemplateValue(templateKey)"
@update:model-value="updateTemplateValue(templateKey, $event)"
type="number"
density="compact"
variant="outlined"
hide-details
:placeholder="t('core.common.objectEditor.placeholders.numberValue')"
:style="template.slider ? 'max-width: 120px;' : ''"
></v-text-field>
</div>
<v-switch
v-else-if="template.type === 'boolean' || template.type === 'bool'"
:model-value="getTemplateValue(templateKey)"
@update:model-value="updateTemplateValue(templateKey, $event)"
density="compact"
hide-details
color="primary"
></v-switch>
</v-col>
<v-col cols="1" class="pl-2">
<v-btn
v-if="isTemplateKeyAdded(templateKey)"
icon
variant="text"
size="small"
color="error"
@click="removeTemplateKey(templateKey)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
</div>
<div v-if="localKeyValuePairs.length === 0 && !hasTemplateSchema" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon>
<p class="text-grey mt-4">{{ t('core.common.objectEditor.noParams') }}</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="t('core.common.objectEditor.newKeyLabel')"
density="compact"
variant="outlined"
hide-details
class="flex-grow-1"
></v-text-field>
<v-select
v-model="newValueType"
:items="['string', 'number', 'boolean', 'json']"
:label="t('core.common.objectEditor.valueTypeLabel')"
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>
{{ t('core.common.add') }}
</v-btn>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelDialog">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="primary" @click="confirmDialog">{{ t('core.common.confirm') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useI18n, useModuleI18n } from '@/i18n/composables'
const { t } = useI18n()
const { tm, getRaw } = useModuleI18n('features/config-metadata')
const props = defineProps({
modelValue: {
type: Object,
required: true
},
itemMeta: {
type: Object,
default: null
},
buttonText: {
type: String,
default: ''
},
dialogTitle: {
type: String,
default: ''
},
maxDisplayItems: {
type: Number,
default: 1
}
})
const emit = defineEmits(['update:modelValue'])
const resolveButtonText = computed(() => props.buttonText || t('core.common.list.modifyButton'))
const resolveDialogTitle = computed(() => props.dialogTitle || t('core.common.objectEditor.dialogTitle'))
const dialog = ref(false)
const localKeyValuePairs = ref([])
const originalKeyValuePairs = ref([])
const newKey = ref('')
const newValueType = ref('string')
// Template schema support
const templateSchema = computed(() => {
return props.itemMeta?.template_schema || {}
})
const hasTemplateSchema = computed(() => {
return Object.keys(templateSchema.value).length > 0
})
// 计算要显示的键名
const displayKeys = computed(() => {
return Object.keys(props.modelValue).slice(0, props.maxDisplayItems)
})
// 分离模板字段和普通字段
const nonTemplatePairs = computed(() => {
return localKeyValuePairs.value.filter(pair => !templateSchema.value[pair.key])
})
// 监听 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)) {
let _type = (typeof value) === 'object' ? 'json':(typeof value)
let _value = _type === 'json'?JSON.stringify(value):value
// Check if this key has a template schema
const template = templateSchema.value[key]
if (template) {
// Use template type if available
_type = template.type || _type
// Use template default if value is missing
if (_value === undefined || _value === null) {
_value = template.default !== undefined ? template.default : _value
}
}
localKeyValuePairs.value.push({
key: key,
value: _value,
type: _type,
slider: template?.slider,
template: template
})
}
}
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(t('core.common.objectEditor.keyExists'))
return
}
let defaultValue
switch (newValueType.value) {
case 'number':
defaultValue = 0
break
case 'boolean':
defaultValue = false
break
case 'json':
defaultValue = "{}"
break
default: // string
defaultValue = ""
break
}
localKeyValuePairs.value.push({
key: key,
value: defaultValue,
type: newValueType.value
})
newKey.value = ''
}
}
function updateJSON(index, newValue) {
try {
JSON.parse(newValue)
localKeyValuePairs.value[index].jsonError = ''
} catch (e) {
localKeyValuePairs.value[index].jsonError = t('core.common.objectEditor.invalidJson')
}
}
function removeKeyValuePairByKey(key) {
const index = localKeyValuePairs.value.findIndex(pair => pair.key === key)
if (index >= 0) {
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(t('core.common.objectEditor.keyExists'))
// 将键名恢复为修改前的原始值
localKeyValuePairs.value[index].key = originalKey
return
}
// 检查新键名是否有模板
const template = templateSchema.value[newKey]
if (template) {
// 更新类型和默认值
localKeyValuePairs.value[index].type = template.type || localKeyValuePairs.value[index].type
if (localKeyValuePairs.value[index].value === undefined || localKeyValuePairs.value[index].value === null || localKeyValuePairs.value[index].value === '') {
localKeyValuePairs.value[index].value = template.default !== undefined ? template.default : localKeyValuePairs.value[index].value
}
localKeyValuePairs.value[index].slider = template.slider
localKeyValuePairs.value[index].template = template
} else {
// 清除模板信息
localKeyValuePairs.value[index].slider = undefined
localKeyValuePairs.value[index].template = undefined
}
// 更新本地副本
localKeyValuePairs.value[index].key = newKey
}
function isTemplateKeyAdded(templateKey) {
return localKeyValuePairs.value.some(pair => pair.key === templateKey)
}
function getTemplateValue(templateKey) {
const pair = localKeyValuePairs.value.find(pair => pair.key === templateKey)
if (pair) {
return pair.value
}
const template = templateSchema.value[templateKey]
return template?.default !== undefined ? template.default : getDefaultValueForType(template?.type || 'string')
}
function updateTemplateValue(templateKey, newValue) {
const existingIndex = localKeyValuePairs.value.findIndex(pair => pair.key === templateKey)
const template = templateSchema.value[templateKey]
if (existingIndex >= 0) {
// 更新现有值
localKeyValuePairs.value[existingIndex].value = newValue
} else {
// 添加新字段
let valueType = template?.type || 'string'
localKeyValuePairs.value.push({
key: templateKey,
value: newValue,
type: valueType,
slider: template?.slider,
template: template
})
}
}
function removeTemplateKey(templateKey) {
const index = localKeyValuePairs.value.findIndex(pair => pair.key === templateKey)
if (index >= 0) {
localKeyValuePairs.value.splice(index, 1)
}
}
function getDefaultValueForType(type) {
switch (type) {
case 'int':
case 'float':
case 'number':
return 0
case 'bool':
case 'boolean':
return false
case 'json':
return "{}"
case 'string':
default:
return ""
}
}
function confirmDialog() {
const updatedValue = {}
for (const pair of localKeyValuePairs.value) {
if (pair.type === 'json' && pair.jsonError) return
let convertedValue = pair.value
// 根据声明的类型进行转换
switch (pair.type) {
case 'int':
convertedValue = parseInt(pair.value) || 0
break
case 'float':
case 'number':
// 尝试转换为数字,如果失败则保持原值(或设为默认值0)
convertedValue = Number(pair.value)
// 可选:检查是否为有效数字,无效则设为0或报错
// if (isNaN(convertedValue)) convertedValue = 0;
break
case 'bool':
case 'boolean':
// 布尔值通常由 v-switch 正确处理,但为保险起见可以显式转换
// 注意:在 JavaScript 中,只有严格的 false, 0, "", null, undefined, NaN 会被转换为 false
// 这里直接赋值 pair.value 应该是安全的,因为 v-model 绑定的就是布尔值
// convertedValue = Boolean(pair.value)
break
case 'json':
convertedValue = JSON.parse(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
}
function translateIfKey(value) {
if (!value || typeof value !== 'string') return value
return getRaw(value) ? tm(value) : value
}
function getTemplateTitle(template, templateKey) {
return translateIfKey(template?.name || template?.description || templateKey)
}
</script>
<style scoped>
.key-value-pair {
width: 100%;
}
.template-field {
transition: opacity 0.2s;
}
.template-field-inactive {
opacity: 0.8;
}
</style>