339 lines
8.3 KiB
Vue

<template>
<component
:is="!!label ? 'BorderLabel' : 'div'"
:label="label"
:icon="icon"
:model-value="isCollapse"
>
<div class="dict-editor">
<div
v-for="(item, index) in allItems"
:key="`${item.type}-${index}`"
class="row q-col-gutter-sm items-center"
>
<div class="col-4">
<template v-if="item.type === 'fixed'">
<q-input
:model-value="item.key"
:label="getKeyLabel(item.key)"
dense
filled
readonly
disable
>
<template v-slot:prepend>
<q-icon name="lock" />
</template>
</q-input>
</template>
<template v-else>
<q-input
v-if="options?.optionKeys"
:model-value="item.key"
label="名称"
dense
filled
@update:model-value="(val) => handleInput(val, index)"
>
<template v-slot:prepend>
<q-icon name="code" />
</template>
<template v-slot:append>
<q-btn dense flat icon="menu">
<q-menu>
<q-list dense>
<q-item
v-for="opt in normalizedOptionKeys"
:key="opt.value"
clickable
v-close-popup
@click="handleSelect(opt, index)"
>
<q-item-section>
{{ getKeyLabel(opt) }}
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</template>
</q-input>
<q-input
v-else
:model-value="item.key"
label="名称"
dense
filled
@update:model-value="(val) => handleInput(val, index)"
>
<template v-slot:prepend>
<q-icon name="code" />
</template>
</q-input>
</template>
</div>
<div class="col">
<VariableInput
:model-value="item.value"
label=""
icon="code"
class="col-grow"
@update:model-value="(val) => updateItemValue(val, index)"
/>
</div>
<div
v-if="item.type !== 'fixed' && !options?.disableAdd"
class="col-auto"
>
<div class="btn-container">
<template v-if="localItems.length === 1">
<q-btn
flat
dense
size="sm"
icon="add"
class="center-btn"
@click="addItem"
/>
</template>
<template v-else-if="index === allItems.length - 1">
<q-btn
flat
dense
size="sm"
icon="remove"
class="top-btn"
@click="removeItem(index)"
/>
<q-btn
flat
dense
size="sm"
icon="add"
class="bottom-btn"
@click="addItem"
/>
</template>
<template v-else>
<q-btn
flat
dense
size="sm"
icon="remove"
class="center-btn"
@click="removeItem(index)"
/>
</template>
</div>
</div>
</div>
</div>
</component>
</template>
<script>
import { defineComponent } from "vue";
import { newVarInputVal } from "js/composer/varInputValManager";
import VariableInput from "components/composer/common/VariableInput.vue";
import BorderLabel from "components/composer/common/BorderLabel.vue";
export default defineComponent({
name: "DictEditor",
components: {
VariableInput,
BorderLabel,
},
props: {
modelValue: {
type: Object,
default: () => ({}),
},
options: {
type: Object,
default: () => ({}),
},
label: {
type: String,
default: "",
},
icon: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
isCollapse: {
type: Boolean,
default: true,
},
},
emits: ["update:modelValue"],
created() {
if (!this.modelValue || Object.keys(this.modelValue).length === 0) {
this.$emit("update:modelValue", { "": newVarInputVal("str") });
}
},
data() {
return {
inputValue: "",
};
},
computed: {
modelEntries() {
return Object.entries(this.modelValue || {});
},
fixedItems() {
const fixedKeys = this.normalizeKeys(this.options?.fixedKeys || []);
return fixedKeys.map((key) => ({
type: "fixed",
key: key.value,
value:
this.modelEntries.find(([k]) => k === key.value)?.[1] ||
newVarInputVal("str"),
}));
},
localItems() {
return this.modelEntries
.filter(
([key]) =>
!this.normalizeKeys(this.options?.fixedKeys || []).some(
(k) => k.value === key
)
)
.map(([key, value]) => ({
type: "editable",
key,
value,
}));
},
allItems() {
return [...this.fixedItems, ...this.localItems];
},
normalizedOptionKeys() {
return this.normalizeKeys(this.options?.optionKeys || []);
},
},
methods: {
normalizeKeys(keys) {
return keys.map((key) =>
typeof key === "string" ? { value: key, label: key } : key
);
},
getKeyLabel(key) {
if (typeof key === "object") return key.label;
const allKeys = [
...this.normalizeKeys(this.options?.fixedKeys || []),
...this.normalizedOptionKeys,
];
return allKeys.find((k) => k.value === key)?.label || key;
},
updateItemValue(val, index) {
const item = this.allItems[index];
const dict = { ...this.modelValue };
dict[item.key] = val;
this.$emit("update:modelValue", dict);
},
addItem() {
if (this.options?.disableAdd) return;
const dict = { ...this.modelValue };
dict[""] = newVarInputVal("str");
this.$emit("update:modelValue", dict);
},
removeItem(index) {
const item = this.localItems[index];
const dict = { ...this.modelValue };
delete dict[item.key];
// 如果删除后没有条目,添加一个空行
if (Object.keys(dict).length === 0) {
dict[""] = newVarInputVal("str");
}
this.$emit("update:modelValue", dict);
},
handleInput(val, index) {
this.inputValue = val;
const item = this.allItems[index];
if (val in this.modelValue && val !== item.key) {
return;
}
const dict = { ...this.modelValue };
delete dict[item.key];
dict[val] = item.value;
this.$emit("update:modelValue", dict);
},
handleSelect(val, index) {
this.inputValue = "";
const value = val?.value || val;
const item = this.allItems[index];
if (value in this.modelValue && value !== item.key) {
return;
}
const dict = { ...this.modelValue };
delete dict[item.key];
dict[value] = item.value;
this.$emit("update:modelValue", dict);
},
handleBlur() {
this.inputValue = "";
},
},
});
</script>
<style scoped>
.dict-editor {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
/* 防止输入框换行 */
:deep(.q-field__native) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn-container {
position: relative;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-container .q-btn {
position: absolute;
width: 16px;
height: 16px;
min-height: 16px;
padding: 0;
}
.btn-container .center-btn {
position: relative;
}
.btn-container .top-btn {
top: 0;
}
.btn-container .bottom-btn {
bottom: 0;
}
:deep(.q-btn .q-icon) {
font-size: 14px;
}
:deep(.q-btn.q-btn--dense) {
padding: 0;
min-height: 16px;
}
</style>