多分辨率预览组件
多分辨率预览组件,基础组件来源于
ant-design-vue
。
gitee地址:https://gitee.com/wkjgit/multi-resolution-preview.git
在构建诸如大屏应用或推广页面生成器这般拥有页面生成功能的系统时,通常需查看不同分辨率下预览页面效果,而多分辨率预览组件正是为此而设。
原型
预览组件
多分辨率预览组件
组件功能
- 预览组件,支持自定义分辨率、缩放比例等;
- 多分辨率预览组件,支持多分辨率下同时预览。
实现思路
组件划分思路
- 首先明确多分辨率预览组件是基于预览组件开发的,所以从大的角度上划分为预览组件及多分辨率预览组件;
- 预览组件包含菜单区、渲染区两个部分,其中渲染区可通过菜单设置缩放比例,也就是说渲染区是支持动态调整显示缩放比例。由于多分辨率预览组件主要是为页面生成系统服务的,在页面生成系统中可视化设计器部分极大可能需要支持设计画布的缩放,所以将可缩放容器单独抽离出来,形成独立组件;由于菜单区域功能并不复杂,且没有其他地方需要使用,所以不将其单独提出。
因此整个组件单元,包括:
ScaleContainer
- 可缩放容器;ResponsiveView
- 预览组件;MultiResolutionView
- 多分辨率预览组件
关键点实现说明
-
可缩放容器实现思路
一开始说到缩放的时候肯定会想到使用css3
中的scale
去实现。但是,通过css进行缩放后虽然从显示上看确实达到了缩放的效果,但占位并没有缩放。
那么只要解决这个问题,就能很好的实现组件功能了。如何解决占位问题呢,都知道,定位为:
absolute
时,元素就会脱离文档流,那只要让其脱离文档流,并且保证在文档流中有对应的占位效果,那就能解决这个问题了。具体实现,见后方代码。
组件源码
ScaleContainer
- 可缩放容器
<!--
* @Description: 缩放容器
* @Author: wang keju
* @Email: git config user.email
* @Date: 2025-02-04 14:10:57
* @LastEditTime: 2025-02-04 21:51:31
* @LastEditors: wang keju
-->
<script lang="ts" setup>
import { computed, type CSSProperties } from "vue";
export type ScaleViewProps = {
contentWidth: number;
contentHeight: number;
showMask?: boolean;
canWheel?: boolean;
scale: number;
}
export type ScaleViewEmits = {
(event: "update:scale", scale: number): void;
}
export type ScaleViewExpose = {}
const props = withDefaults(defineProps<ScaleViewProps>(), {
showMask: false,
canWheel: true,
scale: 1,
});
const emits = defineEmits<ScaleViewEmits>();
const containerStyle = computed<CSSProperties>(() => ({
width: `${props.contentWidth}px`,
height: `${props.contentHeight}px`,
transform: `scale(${props.scale})`,
}));
const blockStyle = computed<CSSProperties>(() => ({
width: `${props.contentWidth * props.scale}px`,
height: `${props.contentHeight * props.scale}px`,
}));
const onWheel = (e: WheelEvent) => {
if (e.ctrlKey && props.canWheel) {
e.preventDefault();
if (e.deltaY < 0) {
emits('update:scale', props.scale + 0.01);
} else {
emits('update:scale', props.scale - 0.01);
}
}
}
</script>
<template>
<div class="scale-view-wrapper">
<div class="scale-view-container" :style="blockStyle" @wheel="onWheel">
<div :style="blockStyle"></div>
<div class="scale-view-mask" :style="blockStyle" v-if="showMask"></div>
<div class="scale-view-content" :style="containerStyle">
<slot />
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.scale-view-wrapper {
width: 100%;
height: 100%;
overflow: auto;
user-select: none;
.scale-view-container {
position: relative;
overflow: hidden;
margin: 0 auto;
background-color: #fff;
.scale-view-mask {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
.scale-view-content {
position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;
}
}
}
</style>
ResponsiveView
- 预览组件
<script lang="ts" setup>
import { computed, onBeforeMount, ref, watch } from 'vue';
import { CaretDownOutlined } from "@ant-design/icons-vue";
import ScaleContainer from '../ScaleContainer/ScaleContainer.vue';
type Props = {
src: string;
dev?: "responsive" | "minMobileDevice" | "middleMobileDevice" | "largeMobileDevice" | "tablet" | "laptop" | "largeLaptop";
autoSize?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
dev: 'responsive',
autoSize: false
});
const currentScale = ref<number>(0.5);
const sizeType = ref<string>(props.dev);
const screenWidth = ref<number>(981);
const screenHeight = ref<number>(840);
const deviceInfoList = [
{ type: 'responsive', label: '自适应', width: 981, height: 840, widthSetDisabled: true, heightSetDisabled: false },
{ type: 'minMobileDevice', label: '小型移动端', width: 375, height: 667, widthSetDisabled: false, heightSetDisabled: true },
{ type: 'middleMobileDevice', label: '中型移动端', width: 414, height: 736, widthSetDisabled: false, heightSetDisabled: true },
{ type: 'largeMobileDevice', label: '大型移动端', width: 768, height: 1024, widthSetDisabled: false, heightSetDisabled: true },
{ type: 'tablet', label: '平板', width: 1024, height: 768, widthSetDisabled: false, heightSetDisabled: true },
{ type: 'laptop', label: '笔记本', width: 1280, height: 800, widthSetDisabled: false, heightSetDisabled: true },
{ type: 'largeLaptop', label: '大型笔记本', width: 1440, height: 900, widthSetDisabled: false, heightSetDisabled: true },
]
const widthSetDisabled = computed<boolean>(() => {
if (sizeType.value !== 'responsive') return true;
return false;
});
const heightSetDisabled = computed<boolean>(() => {
if (sizeType.value !== 'responsive') return true;
return false;
});
const typeName = computed(() => {
return deviceInfoList.find(item => item.type === sizeType.value)?.label || '自适应'
})
const changeSizeType = (type: string) => {
sizeType.value = type;
}
const changeScale = (scale: number) => {
currentScale.value = scale;
}
const onSizeTypeChange = () => {
const deviceInfo = deviceInfoList.find(item => item.type === sizeType.value);
screenWidth.value = deviceInfo?.width || 0;
screenHeight.value = deviceInfo?.height || 0;
}
watch(sizeType, onSizeTypeChange);
onBeforeMount(onSizeTypeChange);
</script>
<template>
<div class="responsive-view-wrapper" :class="{ 'auto-size': props.autoSize }">
<div class="responsive-view-tools">
<ADropdown>
<AButton type="text" size="small" style="margin-right: 12px;">
{{ typeName }}<CaretDownOutlined />
</AButton>
<template #overlay>
<AMenu @click="({key}) => changeSizeType(key as any)">
<AMenuItem v-for="dev in deviceInfoList" :key="dev.type">{{ dev.label }}</AMenuItem>
</AMenu>
</template>
</ADropdown>
<ASpace>
<AInputNumber size="small" :disabled="widthSetDisabled" :min="100" v-model:value="screenWidth" style="width: 60px;" />
<span>X</span>
<AInputNumber size="small" :disabled="heightSetDisabled" :min="100" v-model:value="screenHeight" style="width: 60px;" />
</ASpace>
<ADropdown>
<AButton type="text" size="small" style="margin-right: 12px;">
{{ Math.ceil(currentScale * 100) }}%
<CaretDownOutlined />
</AButton>
<template #overlay>
<AMenu @click="({key}) => changeScale(key as any)">
<AMenuItem :key="0.5">50%</AMenuItem>
<AMenuItem :key="0.75">75%</AMenuItem>
<AMenuItem :key="1">100%</AMenuItem>
<AMenuItem :key="1.25">125%</AMenuItem>
<AMenuItem :key="1.5">150%</AMenuItem>
<AMenuItem :key="2">200%</AMenuItem>
</AMenu>
</template>
</ADropdown>
</div>
<div class="responsive-view">
<div style="width: 100%; height: 100%;">
<ScaleContainer :content-width="screenWidth" :content-height="screenHeight" :show-mask="true" v-model:scale="currentScale">
<iframe :src="src" style="width: 100%; height: 100%; border: none;" />
</ScaleContainer>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.responsive-view-wrapper {
display: inline-flex;
flex-flow: column;
box-sizing: border-box;
border: 1px solid #ddd;
user-select: none;
&:not(.auto-size) {
width: 100%;
height: 100%;
}
&:is(.auto-size) {
min-width: 380px;
}
.responsive-view-tools {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 28px;
font-size: 12px;
}
.responsive-view {
flex-shrink: 1;
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
padding: 24px;
background-color: #efefef;
}
}
</style>
MultiResolutionPreview
- 多分辨率预览组件
<script lang="ts" setup>
import ResponsiveView from '../ResponsiveView/ResponsiveView.vue';
</script>
<template>
<div class="responsive-view">
<ResponsiveView dev="minMobileDevice" :auto-size="true" src="http://localhost:5173/demo" />
<ResponsiveView dev="middleMobileDevice" :auto-size="true" src="http://localhost:5173/demo" />
<ResponsiveView dev="largeMobileDevice" :auto-size="true" src="http://localhost:5173/demo" />
<ResponsiveView dev="tablet" :auto-size="true" src="http://localhost:5173/demo" />
<ResponsiveView dev="laptop" :auto-size="true" src="http://localhost:5173/demo" />
<ResponsiveView dev="largeLaptop" :auto-size="true" src="http://localhost:5173/demo" />
<ResponsiveView dev="responsive" :auto-size="true" src="http://localhost:5173/demo" />
</div>
</template>
<style lang="less" scoped>
.responsive-view {
display: flex;
flex-wrap: wrap;
width: 100%;
height: 100%;
overflow: auto;
& > div {
margin-left: 6px;
margin-bottom: 12px;
}
}
</style>