Skip to content

可视化适配

公共组件

HTML
<template>
  <section
    :style="{ ...styles.box, ...boxStyle }"
    class="v-screen-box"
    ref="box"
  >
    <div
      :style="{ ...styles.wrapper, ...wrapperStyle }"
      class="screen-wrapper"
      ref="screenWrapper"
    >
      <slot></slot>
    </div>
  </section>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import type { CSSProperties, PropType } from "vue";
/**
 * 防抖函数
 * @param {Function} fn
 * @param {number} delay
 * @returns {() => void}
 */
function debounce(fn: Function, delay: number): () => void {
  // let timer: NodeJS.Timer;
  let timer: any;
  return function (...args: any[]): void {
    if (timer) clearTimeout(timer);
    timer = setTimeout(
      () => {
        typeof fn === "function" && fn.apply(null, args);
        clearTimeout(timer);
      },
      delay > 0 ? delay : 100
    );
  };
}

interface IState {
  originalWidth: string | number;
  originalHeight: string | number;
  width?: string | number;
  height?: string | number;
  observer: null | MutationObserver;
}
type IAutoScale =
  | boolean
  | {
      x?: boolean;
      y?: boolean;
    };
const props = defineProps({
  width: {
    type: [String, Number] as PropType<string | number>,
    default: 1920,
  },
  height: {
    type: [String, Number] as PropType<string | number>,
    default: 1080,
  },
  fullScreen: {
    type: Boolean as PropType<boolean>,
    default: false,
  },
  autoScale: {
    type: [Object, Boolean] as PropType<IAutoScale>,
    default: true,
  },
  delay: {
    type: Number as PropType<number>,
    default: 500,
  },
  boxStyle: {
    type: Object as PropType<CSSProperties>,
    default: () => ({}),
  },
  wrapperStyle: {
    type: Object as PropType<CSSProperties>,
    default: () => ({}),
  },
});

const state = reactive<IState>({
  width: 0,
  height: 0,
  originalWidth: 0,
  originalHeight: 0,
  observer: null,
});

const styles: Record<string, CSSProperties> = {
  box: {
    overflow: "hidden",
    backgroundSize: `100% 100%`,
    background: `#000000`,
    width: `100vw`,
    height: `100vh`,
  },
  wrapper: {
    transitionProperty: `all`,
    transitionTimingFunction: `cubic-bezier(0.4, 0, 0.2, 1)`,
    transitionDuration: `500ms`,
    position: `relative`,
    overflow: `hidden`,
    zIndex: 100,
    transformOrigin: `left top`,
  },
};

const screenWrapper = ref<HTMLElement>();
const box = ref<HTMLElement>();

watch(
  () => props.autoScale,
  async (newVal: any) => {
    if (newVal) {
      onResize();
      addListener();
    } else {
      clearListener();
      clearScreenWrapperStyle();
    }
  }
);
/**
 * 初始化大屏容器宽高
 */
const initSize = () => {
  return new Promise<void>((resolve) => {
    box.value!.scrollLeft = 0;
    box.value!.scrollTop = 0;
    nextTick(() => {
      // region 获取大屏真实尺寸
      if (props.width && props.height) {
        state.width = props.width;
        state.height = props.height;
      } else {
        state.width = screenWrapper.value?.clientWidth;
        state.height = screenWrapper.value?.clientHeight;
      }
      // endregion

      // region 获取画布尺寸
      if (!state.originalHeight || !state.originalWidth) {
        state.originalWidth = window.screen.width;
        state.originalHeight = window.screen.height;
      }
      // endregion
      resolve();
    });
  });
};

/**
 * 更新大屏容器宽高
 */
const updateSize = () => {
  if (state.width && state.height) {
    screenWrapper.value!.style.width = `${state.width}px`;
    screenWrapper.value!.style.height = `${state.height}px`;
  } else {
    screenWrapper.value!.style.width = `${state.originalWidth}px`;
    screenWrapper.value!.style.height = `${state.originalHeight}px`;
  }
};
const clearScreenWrapperStyle = () => {
  screenWrapper.value!.style.transform = "";
  screenWrapper.value!.style.margin = "";
};
const autoScale = (scale: number) => {
  if (!props.autoScale) {
    return;
  }
  const domWidth = screenWrapper.value!.clientWidth;
  const domHeight = screenWrapper.value!.clientHeight;
  const currentWidth = document.body.clientWidth;
  const currentHeight = document.body.clientHeight;
  screenWrapper.value!.style.transform = `scale(${scale},${scale})`;
  let mx = Math.max((currentWidth - domWidth * scale) / 2, 0);
  let my = Math.max((currentHeight - domHeight * scale) / 2, 0);
  if (typeof props.autoScale === "object") {
    !props.autoScale.x && (mx = 0);
    !props.autoScale.y && (my = 0);
  }
  screenWrapper.value!.style.margin = `${my}px ${mx}px`;
};
const updateScale = () => {
  // 获取真实视口尺寸
  const currentWidth = document.body.clientWidth;
  const currentHeight = document.body.clientHeight;
  // 获取大屏最终的宽高
  const realWidth = state.width || state.originalWidth;
  const realHeight = state.height || state.originalHeight;
  // 计算缩放比例
  const widthScale = currentWidth / +realWidth;
  const heightScale = currentHeight / +realHeight;
  // 若要铺满全屏,则按照各自比例缩放
  if (props.fullScreen) {
    screenWrapper.value!.style.transform = `scale(${widthScale},${heightScale})`;
    return false;
  }
  // 按照宽高最小比例进行缩放
  const scale = Math.min(widthScale, heightScale);
  autoScale(scale);
};

const onResize = debounce(async () => {
  await initSize();
  updateSize();
  updateScale();
}, props.delay);
const initMutationObserver = () => {
  const observer = (state.observer = new MutationObserver(() => {
    onResize();
  }));
  observer.observe(screenWrapper.value!, {
    attributes: true,
    attributeFilter: ["style"],
    attributeOldValue: true,
  });
};

const clearListener = () => {
  window.removeEventListener("resize", onResize);
  // state.observer?.disconnect();
};

const addListener = () => {
  window.addEventListener("resize", onResize);
  // initMutationObserver();
};
onMounted(() => {
  nextTick(async () => {
    await initSize();
    updateSize();
    updateScale();
    addListener();
    // initMutationObserver();
  });
});
onUnmounted(() => {
  clearListener();
  // state.observer?.disconnect();
});
</script>
<template>
  <section
    :style="{ ...styles.box, ...boxStyle }"
    class="v-screen-box"
    ref="box"
  >
    <div
      :style="{ ...styles.wrapper, ...wrapperStyle }"
      class="screen-wrapper"
      ref="screenWrapper"
    >
      <slot></slot>
    </div>
  </section>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import type { CSSProperties, PropType } from "vue";
/**
 * 防抖函数
 * @param {Function} fn
 * @param {number} delay
 * @returns {() => void}
 */
function debounce(fn: Function, delay: number): () => void {
  // let timer: NodeJS.Timer;
  let timer: any;
  return function (...args: any[]): void {
    if (timer) clearTimeout(timer);
    timer = setTimeout(
      () => {
        typeof fn === "function" && fn.apply(null, args);
        clearTimeout(timer);
      },
      delay > 0 ? delay : 100
    );
  };
}

interface IState {
  originalWidth: string | number;
  originalHeight: string | number;
  width?: string | number;
  height?: string | number;
  observer: null | MutationObserver;
}
type IAutoScale =
  | boolean
  | {
      x?: boolean;
      y?: boolean;
    };
const props = defineProps({
  width: {
    type: [String, Number] as PropType<string | number>,
    default: 1920,
  },
  height: {
    type: [String, Number] as PropType<string | number>,
    default: 1080,
  },
  fullScreen: {
    type: Boolean as PropType<boolean>,
    default: false,
  },
  autoScale: {
    type: [Object, Boolean] as PropType<IAutoScale>,
    default: true,
  },
  delay: {
    type: Number as PropType<number>,
    default: 500,
  },
  boxStyle: {
    type: Object as PropType<CSSProperties>,
    default: () => ({}),
  },
  wrapperStyle: {
    type: Object as PropType<CSSProperties>,
    default: () => ({}),
  },
});

const state = reactive<IState>({
  width: 0,
  height: 0,
  originalWidth: 0,
  originalHeight: 0,
  observer: null,
});

const styles: Record<string, CSSProperties> = {
  box: {
    overflow: "hidden",
    backgroundSize: `100% 100%`,
    background: `#000000`,
    width: `100vw`,
    height: `100vh`,
  },
  wrapper: {
    transitionProperty: `all`,
    transitionTimingFunction: `cubic-bezier(0.4, 0, 0.2, 1)`,
    transitionDuration: `500ms`,
    position: `relative`,
    overflow: `hidden`,
    zIndex: 100,
    transformOrigin: `left top`,
  },
};

const screenWrapper = ref<HTMLElement>();
const box = ref<HTMLElement>();

watch(
  () => props.autoScale,
  async (newVal: any) => {
    if (newVal) {
      onResize();
      addListener();
    } else {
      clearListener();
      clearScreenWrapperStyle();
    }
  }
);
/**
 * 初始化大屏容器宽高
 */
const initSize = () => {
  return new Promise<void>((resolve) => {
    box.value!.scrollLeft = 0;
    box.value!.scrollTop = 0;
    nextTick(() => {
      // region 获取大屏真实尺寸
      if (props.width && props.height) {
        state.width = props.width;
        state.height = props.height;
      } else {
        state.width = screenWrapper.value?.clientWidth;
        state.height = screenWrapper.value?.clientHeight;
      }
      // endregion

      // region 获取画布尺寸
      if (!state.originalHeight || !state.originalWidth) {
        state.originalWidth = window.screen.width;
        state.originalHeight = window.screen.height;
      }
      // endregion
      resolve();
    });
  });
};

/**
 * 更新大屏容器宽高
 */
const updateSize = () => {
  if (state.width && state.height) {
    screenWrapper.value!.style.width = `${state.width}px`;
    screenWrapper.value!.style.height = `${state.height}px`;
  } else {
    screenWrapper.value!.style.width = `${state.originalWidth}px`;
    screenWrapper.value!.style.height = `${state.originalHeight}px`;
  }
};
const clearScreenWrapperStyle = () => {
  screenWrapper.value!.style.transform = "";
  screenWrapper.value!.style.margin = "";
};
const autoScale = (scale: number) => {
  if (!props.autoScale) {
    return;
  }
  const domWidth = screenWrapper.value!.clientWidth;
  const domHeight = screenWrapper.value!.clientHeight;
  const currentWidth = document.body.clientWidth;
  const currentHeight = document.body.clientHeight;
  screenWrapper.value!.style.transform = `scale(${scale},${scale})`;
  let mx = Math.max((currentWidth - domWidth * scale) / 2, 0);
  let my = Math.max((currentHeight - domHeight * scale) / 2, 0);
  if (typeof props.autoScale === "object") {
    !props.autoScale.x && (mx = 0);
    !props.autoScale.y && (my = 0);
  }
  screenWrapper.value!.style.margin = `${my}px ${mx}px`;
};
const updateScale = () => {
  // 获取真实视口尺寸
  const currentWidth = document.body.clientWidth;
  const currentHeight = document.body.clientHeight;
  // 获取大屏最终的宽高
  const realWidth = state.width || state.originalWidth;
  const realHeight = state.height || state.originalHeight;
  // 计算缩放比例
  const widthScale = currentWidth / +realWidth;
  const heightScale = currentHeight / +realHeight;
  // 若要铺满全屏,则按照各自比例缩放
  if (props.fullScreen) {
    screenWrapper.value!.style.transform = `scale(${widthScale},${heightScale})`;
    return false;
  }
  // 按照宽高最小比例进行缩放
  const scale = Math.min(widthScale, heightScale);
  autoScale(scale);
};

const onResize = debounce(async () => {
  await initSize();
  updateSize();
  updateScale();
}, props.delay);
const initMutationObserver = () => {
  const observer = (state.observer = new MutationObserver(() => {
    onResize();
  }));
  observer.observe(screenWrapper.value!, {
    attributes: true,
    attributeFilter: ["style"],
    attributeOldValue: true,
  });
};

const clearListener = () => {
  window.removeEventListener("resize", onResize);
  // state.observer?.disconnect();
};

const addListener = () => {
  window.addEventListener("resize", onResize);
  // initMutationObserver();
};
onMounted(() => {
  nextTick(async () => {
    await initSize();
    updateSize();
    updateScale();
    addListener();
    // initMutationObserver();
  });
});
onUnmounted(() => {
  clearListener();
  // state.observer?.disconnect();
});
</script>

缩放

HTML
<scale-screen width="1980" height="1080" :delay="500" :fullScreen="false" >

</scale-screen>
<scale-screen width="1980" height="1080" :delay="500" :fullScreen="false" >

</scale-screen>

替我上班,工资分你一半