可视化适配
公共组件
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>