Files
frp/web/frpc/src/components/KeyValueEditor.vue

185 lines
4.3 KiB
Vue

<template>
<div class="kv-editor">
<template v-if="readonly">
<div v-if="modelValue.length === 0" class="kv-empty"></div>
<div v-for="(entry, index) in modelValue" :key="index" class="kv-readonly-row">
<span class="kv-readonly-key">{{ entry.key }}</span>
<span class="kv-readonly-value">{{ entry.value }}</span>
</div>
</template>
<template v-else>
<div v-for="(entry, index) in modelValue" :key="index" class="kv-row">
<el-input
:model-value="entry.key"
:placeholder="keyPlaceholder"
class="kv-input"
@update:model-value="updateEntry(index, 'key', $event)"
/>
<el-input
:model-value="entry.value"
:placeholder="valuePlaceholder"
class="kv-input"
@update:model-value="updateEntry(index, 'value', $event)"
/>
<button class="kv-remove-btn" @click="removeEntry(index)">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.5 3a1 1 0 01-1 1H13v9a2 2 0 01-2 2H5a2 2 0 01-2-2V4h-.5a1 1 0 010-2H6a1 1 0 011-1h2a1 1 0 011 1h3.5a1 1 0 011 1zM4.118 4L4 4.059V13a1 1 0 001 1h6a1 1 0 001-1V4.059L11.882 4H4.118zM6 2h4v1H6V2z"
fill="currentColor"
/>
</svg>
</button>
</div>
<button class="kv-add-btn" @click="addEntry">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8 2a.5.5 0 01.5.5v5h5a.5.5 0 010 1h-5v5a.5.5 0 01-1 0v-5h-5a.5.5 0 010-1h5v-5A.5.5 0 018 2z"
fill="currentColor"
/>
</svg>
Add
</button>
</template>
</div>
</template>
<script setup lang="ts">
interface KVEntry {
key: string
value: string
}
interface Props {
modelValue: KVEntry[]
keyPlaceholder?: string
valuePlaceholder?: string
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
keyPlaceholder: 'Key',
valuePlaceholder: 'Value',
readonly: false,
})
const emit = defineEmits<{
'update:modelValue': [value: KVEntry[]]
}>()
const updateEntry = (index: number, field: 'key' | 'value', val: string) => {
const updated = [...props.modelValue]
updated[index] = { ...updated[index], [field]: val }
emit('update:modelValue', updated)
}
const addEntry = () => {
emit('update:modelValue', [...props.modelValue, { key: '', value: '' }])
}
const removeEntry = (index: number) => {
const updated = props.modelValue.filter((_, i) => i !== index)
emit('update:modelValue', updated)
}
</script>
<style scoped>
.kv-editor {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.kv-row {
display: flex;
gap: 8px;
align-items: center;
}
.kv-input {
flex: 1;
}
.kv-remove-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: var(--el-fill-color);
color: var(--el-text-color-secondary);
cursor: pointer;
transition: all 0.15s ease;
flex-shrink: 0;
}
.kv-remove-btn svg {
width: 14px;
height: 14px;
}
.kv-remove-btn:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
html.dark .kv-remove-btn:hover {
background: rgba(248, 113, 113, 0.15);
color: #f87171;
}
.kv-add-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: transparent;
color: var(--color-text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
align-self: flex-start;
}
.kv-add-btn svg {
width: 13px;
height: 13px;
}
.kv-add-btn:hover {
background: var(--color-bg-hover);
}
.kv-empty {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.kv-readonly-row {
display: flex;
gap: 8px;
padding: 4px 0;
font-size: 13px;
}
.kv-readonly-key {
color: var(--el-text-color-secondary);
min-width: 100px;
}
.kv-readonly-value {
color: var(--el-text-color-primary);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
</style>