KeyValueEditor.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. <template>
  2. <div class="key-value-editor">
  3. <a-button class="preview-button" type="dashed" block @click="modalVisible = true">
  4. {{ '{ ' + objectPreview + '}' }}
  5. </a-button>
  6. <a-modal
  7. v-model:open="modalVisible"
  8. title="键值编辑"
  9. width="940px"
  10. :footer="null"
  11. destroy-on-close
  12. >
  13. <div class="key-value-container">
  14. <div
  15. v-for="item in localItems"
  16. :key="item.ukey"
  17. class="key-value-item"
  18. >
  19. <a-input
  20. v-model:value="item.key"
  21. placeholder="键"
  22. class="key-input"
  23. @blur="updateKey(item)"
  24. />
  25. <value-editor
  26. v-model:modelValue="item.value"
  27. @update:modelValue="updateValue"
  28. :forceOneLevel="forceOneLevel"
  29. class="value-input"
  30. />
  31. <a-popconfirm
  32. title="确定要删除这个项吗?"
  33. ok-text="确认"
  34. cancel-text="取消"
  35. @confirm="removeItem(item.key)"
  36. >
  37. <a-button type="text" danger class="item-remove">
  38. 删除
  39. </a-button>
  40. </a-popconfirm>
  41. </div>
  42. <a-button type="dashed" block @click="addItem">
  43. <plus-outlined /> 添加项
  44. </a-button>
  45. </div>
  46. <a-button type="primary" block @click="save">
  47. 保存
  48. </a-button>
  49. </a-modal>
  50. </div>
  51. </template>
  52. <script setup lang="ts">
  53. import { ref, computed, watch } from 'vue';
  54. import { PlusOutlined } from '@ant-design/icons-vue';
  55. import ValueEditor from './ValueEditor.vue';
  56. import { RandomUtils } from '@imengyu/imengyu-utils';
  57. const props = defineProps<{
  58. modelValue?: Record<string, any>;
  59. /**
  60. * 这会限制用户只能创建简单的值,而不能嵌套对象或数组。
  61. * @default false
  62. */
  63. forceOneLevel?: boolean;
  64. /**
  65. * 默认创建的项的模板。当用户点击添加项时,会根据这个模板创建新的项。
  66. * @default { key: '', value: '', type: 'string' }
  67. */
  68. defaultCreateTemplate?: Omit<LocalItem, 'ukey'>;
  69. }>();
  70. const emit = defineEmits<{
  71. (e: 'update:modelValue', value: Record<string, any>): void;
  72. }>();
  73. type LocalItemType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null';
  74. type LocalItem = {
  75. key: string;
  76. ukey: string;
  77. value: any;
  78. type: LocalItemType;
  79. };
  80. const modalVisible = ref(false);
  81. const localItems = ref<LocalItem[]>([]);
  82. function save() {
  83. const newObj = localItems.value.reduce((acc, cur) => ({
  84. ...acc,
  85. [cur.key]: cur.value,
  86. }), {} as Record<string, any>);
  87. emit('update:modelValue', newObj);
  88. modalVisible.value = false;
  89. }
  90. const objectPreview = computed(() => {
  91. return localItems.value.map((item) => {
  92. let value = '';
  93. if (item.type === 'object' || item.type === 'array') {
  94. value = '...';
  95. } else if (item.type === 'string') {
  96. value = `'${item.value}'`;
  97. } else if (item.type === 'null') {
  98. value = 'null';
  99. } else {
  100. value = item.value.toString();
  101. }
  102. return `${item.key}: ${value}`
  103. }).join(', ');
  104. });
  105. function getType(value: any): LocalItem['type'] {
  106. if (value === null) {
  107. return 'null';
  108. } else if (typeof value === 'string') {
  109. return 'string';
  110. } else if (typeof value === 'number') {
  111. return 'number';
  112. } else if (typeof value === 'boolean') {
  113. return 'boolean';
  114. } else if (Array.isArray(value)) {
  115. return 'array';
  116. } else if (typeof value === 'object') {
  117. return 'object';
  118. } else {
  119. return 'string';
  120. }
  121. }
  122. // 监听props变化
  123. watch(
  124. () => props.modelValue,
  125. (newValue) => {
  126. if (newValue) {
  127. localItems.value = Object.entries(newValue).map(([key, value]) => ({
  128. key: key || '',
  129. ukey: RandomUtils.genNonDuplicateIDHEX(10),
  130. value,
  131. type: getType(value),
  132. }));
  133. } else {
  134. localItems.value = [];
  135. }
  136. console.log('aaaa', newValue, localItems);
  137. },
  138. { deep: true, immediate: true }
  139. );
  140. // 更新值
  141. const updateValue = () => {
  142. const newObj = localItems.value.reduce((acc, cur) => ({
  143. ...acc,
  144. [cur.key]: cur.value,
  145. }), {} as Record<string, any>);
  146. emit('update:modelValue', newObj);
  147. };
  148. // 获取一个可用的键
  149. const getUseableKey = (item: LocalItem) => {
  150. if (!item.key) {
  151. item.key = 'key';
  152. }
  153. let useableKey = item.key;
  154. let suffix = 1;
  155. const withoutSelfItems = localItems.value.filter((i) => i.ukey !== item.ukey);
  156. // 检查键是否已存在
  157. while (withoutSelfItems.some((item) => item.key === useableKey)) {
  158. useableKey = `${item.key}${suffix}`;
  159. suffix++;
  160. }
  161. return useableKey;
  162. };
  163. // 更新键
  164. const updateKey = (item: LocalItem) => {
  165. // 检查是否有重复的key
  166. if (localItems.value.filter((i) => i.key === item.key).length > 1) {
  167. item.key = getUseableKey(item);
  168. }
  169. };
  170. // 添加项
  171. const addItem = () => {
  172. const template = {
  173. key: `key${localItems.value.length + 1}`,
  174. value: '',
  175. ukey: RandomUtils.genNonDuplicateIDHEX(10),
  176. type: 'string' as LocalItemType,
  177. ...props.defaultCreateTemplate,
  178. };
  179. template.key = getUseableKey(template);
  180. localItems.value.push(template);
  181. updateValue();
  182. };
  183. // 删除项
  184. const removeItem = (key: string) => {
  185. localItems.value = localItems.value.filter((item) => item.key !== key);
  186. updateValue();
  187. };
  188. </script>
  189. <style scoped>
  190. .key-value-editor {
  191. width: 100%;
  192. }
  193. .preview-button {
  194. max-width: 400px;
  195. }
  196. .key-value-container {
  197. width: 100%;
  198. max-height: 500px;
  199. margin-bottom: 12px;
  200. overflow-y: auto;
  201. }
  202. .key-value-item {
  203. display: flex;
  204. align-items: flex-start;
  205. margin-bottom: 8px;
  206. }
  207. .key-input {
  208. width: 120px;
  209. margin-right: 8px;
  210. }
  211. .value-input {
  212. flex: 1;
  213. margin-right: 8px;
  214. }
  215. </style>