index.vue 5.52 KB
<script lang="ts" setup>
  import { onMounted, computed, onUnmounted, unref, ref, watch } from 'vue';
  import { Tooltip } from 'ant-design-vue';
  import { Icon } from '/@/components/Icon';
  import { useFullscreen } from '@vueuse/core';
  import { isNumber, isObject, isString } from '/@/utils/is';
  import AceEditor, { Ace } from 'ace-builds';
  import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url';
  import githubTheme from 'ace-builds/src-noconflict/theme-github?url';
  import 'ace-builds/src-noconflict/mode-json';

  enum EventEnum {
    UPDATE_VALUE = 'update:value',
    CHANGE = 'change',
    BLUR = 'blur',
    FOCUS = 'focus',
  }

  const props = withDefaults(
    defineProps<{
      value?: string | Recordable;
      height?: number | string;
      title?: string;
      disabled?: boolean;
    }>(),
    {
      height: 150,
    }
  );

  const emit = defineEmits<{
    (e: EventEnum.UPDATE_VALUE, value: any, instance?: Ace.Editor): void;
    (e: EventEnum.CHANGE, value: any, instance?: Ace.Editor): void;
    (e: EventEnum.BLUR, event: Event, instance?: Ace.Editor): void;
    (e: EventEnum.FOCUS, event: Event, instance?: Ace.Editor): void;
  }>();

  const jsonEditorElRef = ref<Nullable<any>>();

  const editoreRef = ref<Ace.Editor>();

  const isFocus = ref(false);

  const handleOnChange = () => {
    const value = get();
    emit(EventEnum.UPDATE_VALUE, value, unref(editoreRef));
    emit(EventEnum.CHANGE, value, unref(editoreRef));
  };

  const handleOnBlur = (event: Event) => {
    isFocus.value = false;
    emit(EventEnum.BLUR, event, unref(editoreRef));
  };

  const handleOnFocus = (event: Event) => {
    isFocus.value = true;
    emit(EventEnum.FOCUS, event, unref(editoreRef));
  };

  const getFormatValue = (value: Recordable | string = '') => {
    return isObject(value) ? JSON.stringify(value, null, 2) : value;
  };

  const initialize = () => {
    AceEditor.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl);
    AceEditor.config.setModuleUrl('ace/theme/github', githubTheme);
    const editor = AceEditor.edit(unref(jsonEditorElRef)!, {
      mode: 'ace/mode/json',
    });
    editor.setTheme('ace/theme/github');
    editor.setOptions({
      fontSize: 14,
    });

    editor.on('change', handleOnChange);
    editor.on('blur', handleOnBlur);
    editor.on('focus', handleOnFocus);

    editoreRef.value = editor;
    unref(editoreRef)?.setValue(getFormatValue(props.value), 1);
    unref(editoreRef)?.setReadOnly(props.disabled);
  };

  watch(
    () => props.value,
    (target) => {
      // const position = unref(editoreRef)?.getCursorPosition();
      if (unref(isFocus)) return;
      unref(editoreRef)?.setValue(getFormatValue(target));
      unref(editoreRef)?.clearSelection();
      // position && unref(editoreRef)?.moveCursorToPosition(position!);
    },
    {
      immediate: true,
    }
  );

  watch(
    () => props.disabled,
    (value) => {
      unref(editoreRef)?.setReadOnly(value);
    }
  );

  const get = (): string => {
    return unref(editoreRef)?.getValue() || '';
  };

  const set = (data: any) => {
    return unref(editoreRef)?.setValue(getFormatValue(data));
  };

  onMounted(() => {
    initialize();
    unref(editoreRef)?.setValue(getFormatValue(props.value));
  });

  onUnmounted(() => {
    unref(editoreRef)?.off('change', handleOnChange);
    unref(editoreRef)?.off('blur', handleOnBlur);
    unref(editoreRef)?.off('focus', handleOnFocus);
    unref(editoreRef)?.destroy();
    unref(editoreRef)?.container.remove();
  });

  const handleFormat = () => {
    const value = get();
    if (isString(value) && !value) return;
    unref(editoreRef)?.setValue(JSON.stringify(JSON.parse(value), null, 2));
    unref(editoreRef)?.clearSelection();
  };

  const handleCompress = () => {
    const value = get();
    if (isString(value) && !value) return;
    unref(editoreRef)?.setValue(JSON.stringify(JSON.parse(value)));
    unref(editoreRef)?.clearSelection();
  };

  const jsonEditorContainerElRef = ref<Nullable<HTMLDivElement>>();
  const { isFullscreen, isSupported, toggle } = useFullscreen(jsonEditorContainerElRef);
  const handleFullScreen = () => {
    toggle();
  };

  const getHeight = computed(() => {
    const { height } = props;
    return isNumber(height) ? `${height}px` : height;
  });

  defineExpose({
    get,
    set,
    handleFormat,
  });
</script>

<template>
  <div
    ref="jsonEditorContainerElRef"
    class="p-2 bg-gray-200 flex flex-col"
    :style="{ height: getHeight }"
  >
    <div class="w-full h-8 flex justify-between items-center">
      <div>
        {{ title }}
      </div>
      <slot name="header"></slot>
      <div class="flex h-8 gap-3 justify-end items-center text-dark-500 svg:text-2xl">
        <slot name="beforeFormat"></slot>
        <Tooltip title="整洁">
          <Icon @click="handleFormat" class="cursor-pointer" icon="gg:format-left" />
        </Tooltip>
        <slot name="beforeCompress"></slot>

        <Tooltip title="迷你">
          <Icon @click="handleCompress" class="cursor-pointer" icon="material-symbols:compress" />
        </Tooltip>
        <slot name="beforeFullScreen"></slot>

        <Tooltip title="全屏">
          <Icon
            v-if="isSupported"
            class="cursor-pointer"
            :icon="
              isFullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'
            "
            @click="handleFullScreen"
          />
        </Tooltip>
        <slot name="afterFullScreen"></slot>
      </div>
    </div>
    <div ref="jsonEditorElRef" class="flex-auto"></div>
  </div>
</template>