| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- <template>
- <div class="key-value-editor">
- <a-button class="preview-button" type="dashed" block @click="modalVisible = true">
- {{ '{ ' + objectPreview + '}' }}
- </a-button>
- <a-modal
- v-model:open="modalVisible"
- title="键值编辑"
- width="940px"
- :footer="null"
- destroy-on-close
- >
- <div class="key-value-container">
- <div
- v-for="item in localItems"
- :key="item.ukey"
- class="key-value-item"
- >
- <a-input
- v-model:value="item.key"
- placeholder="键"
- class="key-input"
- @blur="updateKey(item)"
- />
- <value-editor
- v-model:modelValue="item.value"
- @update:modelValue="updateValue"
- :forceOneLevel="forceOneLevel"
- class="value-input"
- />
- <a-popconfirm
- title="确定要删除这个项吗?"
- ok-text="确认"
- cancel-text="取消"
- @confirm="removeItem(item.key)"
- >
- <a-button type="text" danger class="item-remove">
- 删除
- </a-button>
- </a-popconfirm>
- </div>
- <a-button type="dashed" block @click="addItem">
- <plus-outlined /> 添加项
- </a-button>
- </div>
- <a-button type="primary" block @click="save">
- 保存
- </a-button>
- </a-modal>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, computed, watch } from 'vue';
- import { PlusOutlined } from '@ant-design/icons-vue';
- import ValueEditor from './ValueEditor.vue';
- import { RandomUtils } from '@imengyu/imengyu-utils';
- const props = defineProps<{
- modelValue?: Record<string, any>;
- /**
- * 这会限制用户只能创建简单的值,而不能嵌套对象或数组。
- * @default false
- */
- forceOneLevel?: boolean;
- /**
- * 默认创建的项的模板。当用户点击添加项时,会根据这个模板创建新的项。
- * @default { key: '', value: '', type: 'string' }
- */
- defaultCreateTemplate?: Omit<LocalItem, 'ukey'>;
- }>();
- const emit = defineEmits<{
- (e: 'update:modelValue', value: Record<string, any>): void;
- }>();
- type LocalItemType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null';
- type LocalItem = {
- key: string;
- ukey: string;
- value: any;
- type: LocalItemType;
- };
- const modalVisible = ref(false);
- const localItems = ref<LocalItem[]>([]);
- function save() {
- const newObj = localItems.value.reduce((acc, cur) => ({
- ...acc,
- [cur.key]: cur.value,
- }), {} as Record<string, any>);
- emit('update:modelValue', newObj);
- modalVisible.value = false;
- }
- const objectPreview = computed(() => {
- return localItems.value.map((item) => {
- let value = '';
- if (item.type === 'object' || item.type === 'array') {
- value = '...';
- } else if (item.type === 'string') {
- value = `'${item.value}'`;
- } else if (item.type === 'null') {
- value = 'null';
- } else {
- value = item.value.toString();
- }
- return `${item.key}: ${value}`
- }).join(', ');
- });
- function getType(value: any): LocalItem['type'] {
- if (value === null) {
- return 'null';
- } else if (typeof value === 'string') {
- return 'string';
- } else if (typeof value === 'number') {
- return 'number';
- } else if (typeof value === 'boolean') {
- return 'boolean';
- } else if (Array.isArray(value)) {
- return 'array';
- } else if (typeof value === 'object') {
- return 'object';
- } else {
- return 'string';
- }
- }
- // 监听props变化
- watch(
- () => props.modelValue,
- (newValue) => {
- if (newValue) {
- localItems.value = Object.entries(newValue).map(([key, value]) => ({
- key: key || '',
- ukey: RandomUtils.genNonDuplicateIDHEX(10),
- value,
- type: getType(value),
- }));
- } else {
- localItems.value = [];
- }
- console.log('aaaa', newValue, localItems);
-
- },
- { deep: true, immediate: true }
- );
- // 更新值
- const updateValue = () => {
- const newObj = localItems.value.reduce((acc, cur) => ({
- ...acc,
- [cur.key]: cur.value,
- }), {} as Record<string, any>);
- emit('update:modelValue', newObj);
- };
- // 获取一个可用的键
- const getUseableKey = (item: LocalItem) => {
- if (!item.key) {
- item.key = 'key';
- }
-
- let useableKey = item.key;
- let suffix = 1;
-
- const withoutSelfItems = localItems.value.filter((i) => i.ukey !== item.ukey);
- // 检查键是否已存在
- while (withoutSelfItems.some((item) => item.key === useableKey)) {
- useableKey = `${item.key}${suffix}`;
- suffix++;
- }
-
- return useableKey;
- };
- // 更新键
- const updateKey = (item: LocalItem) => {
- // 检查是否有重复的key
- if (localItems.value.filter((i) => i.key === item.key).length > 1) {
- item.key = getUseableKey(item);
- }
- };
- // 添加项
- const addItem = () => {
- const template = {
- key: `key${localItems.value.length + 1}`,
- value: '',
- ukey: RandomUtils.genNonDuplicateIDHEX(10),
- type: 'string' as LocalItemType,
- ...props.defaultCreateTemplate,
- };
- template.key = getUseableKey(template);
- localItems.value.push(template);
- updateValue();
- };
- // 删除项
- const removeItem = (key: string) => {
- localItems.value = localItems.value.filter((item) => item.key !== key);
- updateValue();
- };
- </script>
- <style scoped>
- .key-value-editor {
- width: 100%;
- }
- .preview-button {
- max-width: 400px;
- }
- .key-value-container {
- width: 100%;
- max-height: 500px;
- margin-bottom: 12px;
- overflow-y: auto;
- }
- .key-value-item {
- display: flex;
- align-items: flex-start;
- margin-bottom: 8px;
- }
- .key-input {
- width: 120px;
- margin-right: 8px;
- }
- .value-input {
- flex: 1;
- margin-right: 8px;
- }
- </style>
|