sd-webui-infinite-image-bro.../vue/src/components/KvPairEditor.vue

284 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { deepComputedEffect } from 'vue3-ts-util'
import { message } from 'ant-design-vue'
import { t } from '@/i18n'
interface KVPair {
key: string
value: any
}
interface KVPairLocal {
key: string
value: string
}
interface Props {
modelValue: KVPair
allKeys?: string[]
}
interface Emits {
(e: 'update:modelValue', value: KVPair): void
(e: 'remove'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// Key 错误状态
const keyError = ref('')
// 校验 key 是否有效
const validateKey = (key: string): boolean => {
if (!key.trim()) {
keyError.value = t('keyRequired')
return false
}
if (props.allKeys && props.allKeys.includes(key.trim())) {
keyError.value = t('keyMustBeUnique')
return false
}
keyError.value = ''
return true
}
const modeInitial= () => {
const value = localKv.value.value
if (!value) return 'str' // 空值默认为字符串模式
try {
const parsed = JSON.parse(value)
// 如果解析成功且不是字符串类型,则认为是 JSON 模式
return typeof parsed !== 'string' ? 'json' : 'str'
} catch {
// 解析失败,是字符串模式
return 'str'
}
}
// 组件内部状态:判断当前值是否为 JSON 模式
const mode = ref('str' as 'str' | 'json')
// 本地状态,用于双向绑定
const localKv = deepComputedEffect<KVPairLocal>({
get () {
const val = props.modelValue.value
const isString = typeof val === 'string' && mode.value !== 'json'
return {
...props.modelValue,
value: isString
? val
: JSON.stringify(props.modelValue.value, null, 2)
}
},
set (newValue: KVPairLocal) {
// 校验 key如果无效则不 emit
if (!validateKey(newValue.key)) {
return
}
const value = newValue.value
const allVal = {
...newValue,
value: mode.value === 'json' ? JSON.parse(value) : value
}
emit('update:modelValue', allVal)
}
})
mode.value = modeInitial()
// 判断当前值是否为有效 JSON
const isValidJson = ref(true)
// JSON 输入的 computed
const jsonInput = ref('{}')
watch([() => localKv.value.value, () => mode.value], () => {
if (mode.value === 'json') {
try {
const parsed = JSON.parse(localKv.value.value)
jsonInput.value = JSON.stringify(parsed, null, 2)
} catch (e) {
console.warn('Invalid JSON, resetting jsonInput to empty object', localKv.value.value)
jsonInput.value = '{}'
}
}
},{ immediate: true })
const onJsonUpdate = () => {
const newValue = jsonInput.value
if (checkIsValidJson(newValue)) {
localKv.value.value = newValue
isValidJson.value = true
} else {
isValidJson.value = false
}
}
// 字符串输入的 computed
const stringInput = computed({
get () {
return localKv.value.value
},
set (newValue: string) {
localKv.value.value = newValue
}
})
const checkIsValidJson = (str: string) => {
try {
JSON.parse(str)
return true
} catch {
return false
}
}
// 处理模式切换
const handleModeChange = (newMode: 'str' | 'json') => {
const currentValue = localKv.value.value
// 如果当前值为空,允许切换
if (!currentValue) {
mode.value = newMode
return
}
// 检查是否允许切换
if (newMode === 'json' && currentValue) {
message.warning(t('clearBeforeSwitchToJson'))
return
}
if (newMode === 'str' && jsonInput.value.trim()) {
message.warning(t('clearBeforeSwitchToString'))
console.warn('Switching to string mode requires empty value', {
val: jsonInput.value
})
return
}
localKv.value.value = '' // 切换模式前清空当前值
mode.value = newMode
}
// 处理删除
const handleRemove = () => {
emit('remove')
}
// 暴露校验方法给父组件
const validate = (): boolean => {
const keyValid = validateKey(localKv.value.key)
const jsonValid = mode.value === 'json' ? isValidJson.value : true
return keyValid && jsonValid
}
// 暴露方法和状态给父组件
defineExpose({
validate,
keyError,
isValidJson
})
</script>
<template>
<div class="kv-pair-editor">
<div class="kv-key-wrapper">
<a-input v-model:value="localKv.key" :placeholder="t('keyPlaceholder')" class="kv-input kv-key" />
<div v-if="keyError" class="key-error-hint">{{ keyError }}</div>
</div>
<div v-if="mode === 'json'" class="kv-value-wrapper">
<ATextarea v-model:value="jsonInput" @blur="onJsonUpdate" :placeholder="t('jsonValuePlaceholder')"
:auto-size="{ maxRows: 8 }" class="kv-input kv-value" />
<div v-if="!isValidJson" class="json-error-hint">{{ t('jsonFormatError') }}</div>
</div>
<ATextarea v-else :auto-size="{ maxRows: 8 }" v-model:value="stringInput" :placeholder="t('stringValuePlaceholder')" class="kv-input kv-value" />
<a-select :value="mode" size="small" class="mode-selector"
:getPopupContainer="(trigger: any) => trigger.parentNode as HTMLDivElement" @update:value="handleModeChange"
style="width: 80px">
<a-select-option value="str">{{ t('stringMode') }}</a-select-option>
<a-select-option value="json">{{ t('jsonMode') }}</a-select-option>
</a-select>
<a-button size="small" danger @click="handleRemove" class="delete-btn">
{{ t('delete') }}
</a-button>
</div>
</template>
<style scoped lang="scss">
.kv-pair-editor {
display: flex;
gap: 8px;
align-items: center;
}
.kv-input {
font-size: 12px;
}
.kv-key-wrapper {
min-width: 50px;
max-width: 120px;
display: flex;
flex-direction: column;
gap: 4px;
}
.kv-key {
width: 100%;
}
.kv-value {
min-width: 200px;
}
.kv-value-wrapper {
min-width: 200px;
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.key-error-hint {
font-size: 11px;
color: #ff4d4f;
line-height: 1.2;
}
.json-error-hint {
font-size: 11px;
color: #ff4d4f;
line-height: 1.2;
}
.mode-selector {
font-size: 12px;
flex-shrink: 0;
}
.delete-btn {
font-size: 12px;
flex-shrink: 0;
}
</style>