目录
1.图片组件 image-viewer 编写
- 效果展示:
- 使用该组件时,当前页面会被加上黑色遮罩,屏幕中间会放大图片,屏幕四周会展示一些操作按钮,用于实现轮播、关闭、放大缩小、全屏、旋转等功能
组件名称:TImageViewer
定义关键字段及图标名(全屏 / 恢复原状):- const Mode = {
CONTAIN: {
name: 'contain',
icon: 'el-icon-full-screen',
},
ORIGINAL: {
name: 'original',
icon: 'el-icon-c-scale-to-original',
},
};
定义 图片操作选项 关键字(缩小 / 放大 / 向左旋转 / 向右旋转):- export type ImageViewerAction = 'zoomIn' | 'zoomOut' | 'clocelise' | 'anticlocelise'
定义接受的图片数据类型:- export interface ImageData {
src: string; // 图片地址
desc?: string; // 图片描述信息
}
可供接受的参数(props):props: { // 图片列表 urls: { type: Array as PropType<ImageData[]>, default: () => ([]), }, // 全屏展示时,该组件的默认层级 zIndex: { type: Number, default: 9000, }, // 默认展示的图片编号 initialIndex: { type: Number, default: 0, }, // infinite: { type: Boolean, default: true, }, // hideOnClickModal: { type: Boolean, default: false, }, // 默认图片进入的动画 animateClass: { type: String, default: 'fadeInDown', }, }, // 监听事件 emits: ['close', 'change'],
- 该组件监听(发送)了两个事件:关闭大屏展示的事件 / 改变当前展示图片的事件
- 注意:组件的监听事件需要在 setup() 上方进行声明,关键字段 emits: [ ]
组件模板:<template> <!-- 设置层级 --> <div id="t-image-viewer" class="t-image-viewer" :style="{ zIndex }"> <div class="t-image-viewer__wrapper"> <!-- 遮罩层 --> <div class="t-image-viewer__mask" @click.self="hideOnClickModal && hide()" /> <!-- 关闭按钮 --> <span class="t-image-viewer__btn t-image-viewer__close" @click="hide"> <i class="el-icon-close" /> </span> <!-- 左右切换箭头 --> <template v-if="!isSingle"> <span class="t-image-viewer__btn t-image-viewer__prev" :class="{ 'is-disabled': !infinite && isFirst }" @click="prev" > <i class="el-icon-arrow-left" /> </span> <span class="t-image-viewer__btn t-image-viewer__next" :class="{ 'is-disabled': !infinite && isLast }" @click="next" > <i class="el-icon-arrow-right" /> </span> </template> <!-- 图片描述 --> <div v-if="currentImg.desc" class="t-image-viewer__btn t-image-viewer__desc" > {{ currentImg.desc }} </div> <!-- 操作按钮 --> <div class="t-image-viewer__btn t-image-viewer__actions"> <div class="t-image-viewer__actions__inner"> <!-- 放大 --> <i class="el-icon-zoom-out" @click="handleActions('zoomOut')" /> <!-- 缩小 --> <i class="el-icon-zoom-in" @click="handleActions('zoomIn')" /> <i class="t-image-viewer__actions__divider" /> <!-- 全屏切换 --> <i :class="mode.icon" @click="toggleMode" /> <i class="t-image-viewer__actions__divider" /> <!-- 左旋转 --> <i class="el-icon-refresh-left" @click="handleActions('anticlocelise')" /> <!-- 右旋转 --> <i class="el-icon-refresh-right" @click="handleActions('clocelise')" /> </div> </div> <!-- 大图 --> <div class="t-image-viewer__canvas opacity-hide" :class="[isAddClass === true?'opacity-show animated '+animateClass:'']" > <img v-for="(url, i) in urls" v-show="i === index" ref="img" :key="url" :src="currentImg.src" :style="imgStyle" class="t-image-viewer__img" @load="handleImgLoad" @error="handleImgError" @mousedown="handleMouseDown" /> </div> </div> </div> </template>
- 该模板包含了以下内容:遮罩层、关闭按钮、左右切换箭头、图片描述信息、图片下方操作按钮、图片展示区域
定义需要的变量:let dragHandler = null; // 是否加载中,默认为加载中 const loading = ref(true); // 是否添加动画类名,默认不添加 const isAddClass = ref(false); // 当前图片编号 const index = ref(props.initialIndex); // 图片对象 const img = ref(null); // 当前展示模式,默认全屏展示 const mode = ref(Mode.CONTAIN); // 变换效果 const transform = ref({ scale: 1, deg: 0, offsetX: 0, offsetY: 0, enableTransition: false, }); // 是否显示左右箭头 - 图片数目≤1时,不显示左右箭头 const isSingle = computed(() => { const { urls } = props; return urls.length <= 1; }); // 是否是第一张图片 const isFirst = computed(() => index.value === 0); // 是否是最后一张图片 const isLast = computed(() => index.value === 0); // 当前图片 const currentImg = computed(() => props.urls[index.value]);
图片变化样式事件/** * 图片变化的样式 */ const imgStyle = computed(() => { const { scale, deg, offsetX, offsetY, enableTransition, } = transform.value; const style = { transform: `scale(${scale}) rotate(${deg}deg)`, transition: enableTransition ? 'transform .3s' : '', marginLeft: `${offsetX}px`, marginTop: `${offsetY}px`, } as CSSStyleDeclaration; if (mode.value.name === Mode.CONTAIN.name) { style.maxWidth = '100%'; style.maxHeight = '100%'; } return style; });
关闭事件:/** * 关闭事件 */ function hide() { emit('close'); }
图片加载完成 / 加载失败:/** * 图片加载完成 */ function handleImgLoad() { loading.value = false; isAddClass.value = true; } /** * 图片加载失败 */ function handleImgError() { loading.value = false; }
一些神奇的事件// eslint-disable-next-line no-undef function rafThrottle(fn) { let locked = false; return function (...args: any[]) { if (locked) return; locked = true; window.requestAnimationFrame(() => { fn.apply(this, args); locked = false; }); }; } /* istanbul ignore next */ const on = function ( element: HTMLElement | Document | Window, event: string, handler: EventListenerOrEventListenerObject, useCapture = false, ): void { if (element && event && handler) { element.addEventListener(event, handler, useCapture); } }; /* istanbul ignore next */ const off = function ( element: HTMLElement | Document | Window, event: string, handler: EventListenerOrEventListenerObject, useCapture = false, ) { if (element && event && handler) { element.removeEventListener(event, handler, useCapture); } }; function handleMouseDown(e: MouseEvent) { if (loading.value || e.button !== 0) return; const { offsetX, offsetY } = transform.value; const startX = e.pageX; const startY = e.pageY; dragHandler = rafThrottle((ev) => { transform.value = { ...transform.value, offsetX: offsetX + ev.pageX - startX, offsetY: offsetY + ev.pageY - startY, }; }); on(document, 'mousemove', dragHandler); on(document, 'mouseup', () => { off(document, 'mousemove', dragHandler); }); e.preventDefault(); } function reset() { transform.value = { scale: 1, deg: 0, offsetX: 0, offsetY: 0, enableTransition: false, }; }
图片操作按钮事件:/** * 点击底下的工具条操作 */ function handleActions(action: ImageViewerAction, options = {}) { if (loading.value) return; const { zoomRate, rotateDeg, enableTransition } = { zoomRate: 0.2, rotateDeg: 90, enableTransition: true, ...options, }; if (action === 'zoomOut') { if (transform.value.scale > 0.2) { transform.value.scale = parseFloat((transform.value.scale - zoomRate).toFixed(3)); } } else if (action === 'zoomIn') { transform.value.scale = parseFloat((transform.value.scale + zoomRate).toFixed(3)); } else if (action === 'clocelise') { transform.value.deg += rotateDeg; } else if (action === 'anticlocelise') { transform.value.deg -= rotateDeg; } transform.value.enableTransition = enableTransition; }
切换大小图/** * 切换大小图 */ function toggleMode() { if (loading.value) return; const modeNames = Object.keys(Mode); const modeValues = Object.values(Mode); const currentMode = mode.value.name; const index = modeValues.findIndex((i) => i.name === currentMode); const nextIndex = (index + 1) % modeNames.length; mode.value = Mode[modeNames[nextIndex]]; reset(); }
图片切换/** * 上一张 */ function prev() { if (isFirst.value && !props.infinite) return; const len = props.urls.length; index.value = (index.value - 1 + len) % len; isAddClass.value = false; } /** * 下一张 */ function next() { if (isLast.value && !props.infinite) return; const len = props.urls.length; index.value = (index.value + 1) % len; isAddClass.value = false; }
watch 事件watch(currentImg, () => { nextTick(() => { const $img = img.value; if (!$img.complete) { loading.value = true; } }); }); watch(index, (val) => { reset(); emit('change', val); });
将组件挂在到 body 里onMounted(() => { // 将组件挂载到body标签里 nextTick(() => { const body = document.querySelector('body'); if (body.append) { body.append(document.getElementById('t-image-viewer')); } else { body.appendChild(document.getElementById('t-image-viewer')); } }); });
- 默认样式的设置:
// 固定定位,铺满屏幕 .t-image-viewer{ position:fixed; top:0; right:0; bottom:0; left:0; &__wrapper{ position:fixed; top:0; right:0; bottom:0; left:0; } // 按钮通用样式 &__btn { position: absolute; z-index: 1; display: flex; align-items: center; justify-content: center; border-radius: 50%; opacity: .8; cursor: pointer; box-sizing: border-box; user-select: none; // 控制页面文字不被选中 } // 关闭按钮定位 &__close { top: 40px; right: 40px; } // 大图展示 &__canvas { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; img { margin-bottom: 0 !important; } } // 描述文字 &__desc { left: 50%; bottom: 90px; padding: 0 23px; max-width: calc(100% - 50px); overflow: hidden; font-size: 16px; color: #fff; white-space: nowrap; background-color: rgba($color: #606266, $alpha: .6); border-radius: 22px; transform: translateX(-50%); cursor: default; } // 操作按钮外框 &__actions { left: 50%; bottom: 30px; padding: 0 23px; width: 282px; height: 44px; background-color: #606266; border-radius: 22px; transform: translateX(-50%); } // 操作按钮内容 &__actions__inner { width: 100%; height: 100%; font-size: 23px; color: #fff; text-align: justify; cursor: default; display: flex; align-items: center; justify-content: space-around; i { cursor: pointer; } } &__close, &__next, &__prev { width: 44px; height: 44px; background-color: #606266; font-size: 24px; color: #fff; } &__prev { top: 50%; left: 40px; transform: translateY(-50%); } &__next { top: 50%; right: 40px; text-indent: 2px; transform: translateY(-50%); } &__mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #000; opacity: .5; } .opacity-hide { opacity: 0; } .opacity-show { opacity: 1; } }
2.图片组件 image-base 编写
- 效果展示:
- 用来装图片的小容器,和 image-viewer 搭配使用
组件名称:TImageBase
可供接受的参数(props):/** * t-image-base * @module packages/image-base * @desc 基础图片 * @param {string} [themeStyle] - 主题风格 * @param {string} [fit] - 确定图片如何适应容器框,同原生 object-fit * @param {Array} [previewUrls] - 开启图片预览功能,和image-viewer属性的urls相同 * @param {number} [initialIndex] - 预览的首张图片的位置, 小于等于数组长度 * @param {string} [animateClass] - 图片切换的动画名称,动画采用animate.css * @param {Object} [data] - 数据 * @param {Object} [cStyle] - 自定义样式 * * @example * <t-image-base> * </t-image-base> */ // 主题风格 enum ThemeStyle { LIGHT = 'light', DARK = 'dark', } props: { // 主题风格深浅 themeStyle: { type: String as PropType<ThemeStyle.LIGHT | ThemeStyle.DARK>, default: ThemeStyle.DARK, }, // 确定图片如何适应容器框 fit: { type: String, default: 'fill', }, // 数据 data: { type: Object, default: () => ({}), }, // 图片大图浏览数据 previewUrls: { type: Array, default: () => ([]), }, // 预览的首张图片的位置, 小于等于数组长度 initialIndex: { type: Number, default: 0, }, // 图片切换的动画名称,动画采用animate.css animateClass: { type: String, default: 'fadeInDown', }, // 自定义样式 cStyle: { type: Object, default: () => ({ wrapper: {}, desc: {}, }), }, }, emits: ['image-click', 'change'],
- 注意:该组件通过 emits 给外部提供了两个事件
- image-click
- change
图片列表使用了图片预览动能,也就是1中的那个组件,因此需要进行引入:import TImageViewer from '../../image-viewer/src/index.vue'; components: { TImageViewer, },
组件模板:<template> <div class="t-image-base"> <div class="image" :style="{ ...cStyle.wrapper }"> <div class="image-interval"> <!-- 单张图片容器 --> <img class="image__inner" :src="data.src" :style="{'object-fit':fit}" /> </div> <!-- 图片描述信息 --> <div class="image-desc" :style="{ ...cStyle.desc }" @click="handleImage(data)" > {{ data.desc }} </div> </div> <!-- 如果开启了大屏预览,就展示图片放大器组件 --> <template v-if="preview"> <t-image-viewer v-if="showViewer" :urls="previewUrls" :initial-index="initialIndex" :animate-class="animateClass" @close="closeViewer" @change="changeImage" /> </template> </div> </template>
页面逻辑:// 是否显示大图浏览 const showViewer = ref(false); // 大图浏览的数据 const preview = computed(() => { const { previewUrls } = props; return Array.isArray(previewUrls) && previewUrls.length > 0; }); /** * 打开大图浏览 */ function openViewer() { if (!preview.value) { return; } showViewer.value = true; } /** * 关闭大图浏览 */ function closeViewer() { showViewer.value = false; } /** * 图片切换时 */ function changeImage(index) { emit('change', index); } /** * 图片的点击事件 * @param item 当前点击的图片数据 */ function handleImage(item) { if (preview.value) { openViewer(); } else { emit('image-click', item); } }
- 图片样式:
.t-image-base { .image { position: relative; padding: 2px; width: 100%; height: 89px; border: 1px solid var(--theme-color); border-radius: 5px; cursor: pointer; &-interval { width: 100%; height: 100%; overflow: hidden; } &-desc { position: absolute; top: 0; left: 0; padding: 0 10px; width: 100%; height: 100%; text-align: center; font-size: 14px; color: #fff; text-shadow: 1px 1px 2px #000; display: flex; justify-content: center; align-items: center; } .image__inner { margin: 0; width: 100%; height: 100%; transition: all .5s; } &:hover { .image__inner { transform: scale(1.1); } } } }
3.图片组件 image-base 使用
<el-row :gutter="15" v-if="beautyData.length >= 0"> <el-col v-for="(item, index) in beautyData" :key="index" :span="12" > <t-image-base :data="item" :preview-urls="beautyData" :initial-index="index" :cStyle="{ wrapper: { height: '87px', boxSizing: 'border-box', marginBottom: '17px'}, desc: { boxSizing: 'border-box' }, }" /> </el-col> </el-row> // 图片列表 const beautyData: any = ref([]); // 通过接口填充 - 图片列表 list.forEach((item: any) => { const img = { src: item.url }; beautyData.value.push(img); }); return { beautyData, };
![]()
Vue3 组件示例工程(四) —— 图片组件
最新推荐文章于 2024-06-14 09:41:33 发布