Files
mini-yu/uni_modules/tdesign-uniapp/components/image-viewer/image-viewer.vue
lingxiao865 c5af079d8c first commit
2026-02-10 08:05:03 +08:00

318 lines
8.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view
v-if="dataVisible"
:id="classPrefix"
:class="classPrefix + ' ' + tClass"
:style="tools._style([customStyle, '--td-image-viewer-top: ' + distanceTop + 'px'])"
:aria-modal="true"
aria-role="dialog"
aria-label="图片查看器"
@touchmove.stop.prevent="true"
>
<view
:class="classPrefix + '__mask'"
data-source="overlay"
:style="tools._style([backgroundColor && '--td-image-viewer-mask-bg-color: ' + backgroundColor])"
aria-role="button"
aria-label="关闭"
@click="(e) => onClose(e, 'overlay')"
/>
<block v-if="images && images.length">
<view :class="classPrefix + '__content'">
<swiper
class="swiper"
:style="swiperStyle[currentSwiperIndex] && swiperStyle[currentSwiperIndex].style"
:autoplay="false"
:current="currentSwiperIndex"
tabindex="0"
@change="onSwiperChange"
@click="(e) => onClose(e, '')"
>
<swiper-item
v-for="(item, index) in images"
:key="index"
:class="classPrefix + '__preview-image'"
>
<t-image
v-if="!lazy || shouldLoadImage(index, currentSwiperIndex, loadedImageIndexes)"
:t-class="prefix + '-image--external'"
:class="classPrefix + '__image'"
:custom-style="(imagesStyle[index] && imagesStyle[index].style) || ''"
:data-index="index"
:src="item"
:mode="(imageProps && imageProps.mode) || 'aspectFit'"
:lazy="(imageProps && imageProps.lazy) || false"
:loading="(imageProps && imageProps.loading) || 'default'"
:shape="(imageProps && imageProps.shape) || 'square'"
:webp="(imageProps && imageProps.webp) || false"
:show-menu-by-longpress="(imageProps && imageProps.showMenuByLongpress) || false"
@load="onImageLoadSuccess($event, { index })"
/>
</swiper-item>
</swiper>
</view>
<view
:class="classPrefix + '__nav'"
>
<view
:class="classPrefix + '__nav-close'"
aria-role="button"
aria-label="关闭"
@click.stop.prevent="(e) => onClose(e, '')"
>
<slot name="close-btn" />
<block
v-if="_closeBtn"
name="icon"
>
<t-icon
:custom-style="_closeBtn.style || ''"
:t-class="_closeBtn.tClass"
:prefix="_closeBtn.prefix"
:name="_closeBtn.name"
:size="_closeBtn.size"
:color="_closeBtn.color"
:aria-hidden="!!_closeBtn.ariaHidden"
:aria-label="_closeBtn.ariaLabel"
:aria-role="_closeBtn.ariaRole"
@click="_closeBtn.click || ''"
/>
</block>
</view>
<view
v-if="showIndex"
:class="classPrefix + '__nav-index'"
>
{{ currentSwiperIndex + 1 }}/{{ images.length }}
</view>
<view
:class="classPrefix + '__nav-delete'"
aria-role="button"
aria-label="删除"
@click="onDelete"
>
<slot name="delete-btn" />
<t-icon
v-if="_deleteBtn"
:custom-style="_deleteBtn.style || ''"
:t-class="_deleteBtn.tClass"
:prefix="_deleteBtn.prefix"
:name="_deleteBtn.name"
:size="_deleteBtn.size"
:color="_deleteBtn.color"
:aria-hidden="!!_deleteBtn.ariaHidden"
:aria-label="_deleteBtn.ariaLabel"
:aria-role="_deleteBtn.ariaRole"
@click="_deleteBtn.click || ''"
/>
</view>
</view>
</block>
</view>
</template>
<script>
import TImage from '../image/image';
import TIcon from '../icon/icon';
import { uniComponent } from '../common/src/index';
import { styles, calcIcon, systemInfo } from '../common/utils';
import { prefix } from '../common/config';
import props from './props';
import tools from '../common/utils.wxs';
import { shouldLoadImage } from './computed.js';
import useCustomNavbar from '../mixins/using-custom-navbar';
const name = `${prefix}-image-viewer`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'visible',
event: 'close',
},
],
externalClasses: [`${prefix}-class`],
mixins: [useCustomNavbar],
components: {
TImage,
TIcon,
},
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
currentSwiperIndex: 0,
loadedImageIndexes: [],
windowHeight: 0,
windowWidth: 0,
swiperStyle: {},
imagesStyle: {},
maskTop: 0,
tools,
_deleteBtn: null,
_closeBtn: null,
dataVisible: this.visible,
};
},
watch: {
visible: {
handler(v) {
this.dataVisible = v;
this.init();
},
},
initialIndex: 'init',
images: 'init',
closeBtn: {
handler(v) {
this._closeBtn = calcIcon(v, 'close');
},
immediate: true,
},
deleteBtn: {
handler(v) {
this._deleteBtn = calcIcon(v, 'delete');
},
immediate: true,
},
},
created() {
this.saveScreenSize();
// this.calcMaskTop();
},
mounted() {
this.init();
},
methods: {
shouldLoadImage,
init() {
const { visible: dataVisible, images, initialIndex } = this;
if (dataVisible && images?.length) {
this.loadedImageIndexes = [];
this.currentSwiperIndex = initialIndex >= images.length ? images.length - 1 : initialIndex;
}
},
calcMaskTop() {
if (this.usingCustomNavbar) {
const rect = wx?.getMenuButtonBoundingClientRect() || null;
const { statusBarHeight } = systemInfo;
if (rect && statusBarHeight) {
this.maskTop = rect.top - statusBarHeight + rect.bottom;
}
}
},
saveScreenSize() {
const { windowHeight, windowWidth } = systemInfo;
this.windowHeight = windowHeight;
this.windowWidth = windowWidth;
},
calcImageDisplayStyle(imageWidth, imageHeight) {
const { windowWidth, windowHeight } = this;
const ratio = imageWidth / imageHeight;
// 图片宽高都小于屏幕宽高
if (imageWidth < windowWidth && imageHeight < windowHeight) {
return {
styleObj: {
width: `${imageWidth}px`,
height: `${imageHeight}px`,
left: '50%',
transform: 'translate(-50%, -50%)',
},
};
}
// 图片宽高至少存在一个大于屏幕宽高,此时判断图片宽高比,按长边显示
if (ratio >= 1) {
return {
styleObj: {
width: '100vw',
height: `${(windowWidth / ratio)}px`,
},
};
}
// 图片的高大于宽纵向图设定高度为100vh宽度自适应且确保宽度不超过屏幕宽度
const scaledHeight = ratio * windowHeight * 2;
if (scaledHeight < windowWidth) {
return {
styleObj: {
width: `${scaledHeight}rpx`,
height: '100vh',
left: '50%',
transform: 'translate(-50%, -50%)',
},
};
}
// 当通过高度计算的图片宽度超过屏幕宽度时, 以屏幕宽度为基准, 重新计算高度
return {
styleObj: {
width: '100vw',
height: `${(windowWidth / imageWidth) * imageHeight}px`,
},
};
},
onImageLoadSuccess({ e }, { index }) {
const {
detail: { width, height },
} = e;
const { mode, styleObj } = this.calcImageDisplayStyle(width, height);
const originImagesStyle = this.imagesStyle;
const originSwiperStyle = this.swiperStyle;
if (!this.loadedImageIndexes.includes(index)) {
this.loadedImageIndexes = [...this.loadedImageIndexes, index];
}
this.swiperStyle = {
...originSwiperStyle,
[index]: {
style: `height: ${styleObj.height}`,
},
};
this.imagesStyle = {
...originImagesStyle,
[index]: {
mode,
style: styles({ ...styleObj }),
},
};
},
onSwiperChange(e) {
const {
detail: { current },
} = e;
this.currentSwiperIndex = current;
this._trigger('change', { index: current });
},
onClose(e, source) {
this._trigger('close', { visible: false, trigger: source || 'button', index: this.currentSwiperIndex });
},
onDelete() {
this._trigger('delete', { index: this.currentSwiperIndex });
},
},
});
</script>
<style scoped>
@import './image-viewer.css';
</style>