Files
cmgd-mini-app/uni_modules/nutui-uni/components/imagepreview/imagepreview.vue
2026-01-05 12:47:14 +08:00

278 lines
6.5 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.
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed, defineComponent, onMounted, reactive, watch } from 'vue'
import { CLOSE_EVENT, PREFIX } from '../_constants'
import { funInterceptor, getMainClass } from '../_utils'
import NutIcon from '../icon/icon.vue'
import NutPopup from '../popup/popup.vue'
import NutSwiper from '../swiper/swiper.vue'
import NutSwiperItem from '../swiperitem/swiperitem.vue'
import { imagepreviewEmits, imagepreviewProps } from './imagepreview'
import type { ImageInterface } from './types'
const props = defineProps(imagepreviewProps)
const emit = defineEmits(imagepreviewEmits)
const state = reactive({
showPop: false,
active: 0,
options: {
muted: true,
controls: true,
},
eleImg: null as HTMLElement | null,
store: {
scale: 1,
moveable: false,
originScale: 1,
oriDistance: 1,
},
lastTouchEndTime: 0, // 用来辅助监听双击
})
const styles = computed(() => {
const style: CSSProperties = {
}
if (props.closeIconPosition === 'top-right')
style.right = '10px'
else
style.left = '10px'
return style
})
const classes = computed(() => {
return getMainClass(props, componentName)
})
// 设置当前选中第几个
function setActive(active: number) {
if (active !== state.active) {
state.active = active
emit('change', state.active)
}
}
function closeOnImg() {
if (props.contentClose)
onClose()
}
function onClose() {
funInterceptor(props.beforeClose, {
args: [state.active],
done: () => closeDone(),
})
}
// 执行关闭
function closeDone() {
state.showPop = false
state.store.scale = 1
scaleNow()
emit(CLOSE_EVENT)
}
// 计算两个点的距离
function getDistance(first: { x: number, y: number }, second: { x: number, y: number }) {
return Math.hypot(Math.abs(second.x - first.x), Math.abs(second.y - first.y))
}
function scaleNow() {
if (state.eleImg != null)
state.eleImg.style.transform = `scale(${state.store.scale})`
}
function onTouchStart(event: TouchEvent) {
const curTouchTime = new Date().getTime()
if (curTouchTime - state.lastTouchEndTime < 300) {
const store = state.store
if (store.scale > 1)
store.scale = 1
else if (store.scale === 1)
store.scale = 2
scaleNow()
}
const touches = event.touches
const events = touches[0]
const events2 = touches[1]
const store = state.store
store.moveable = true
if (events2) {
store.oriDistance = getDistance(
{
x: events.pageX,
y: events.pageY,
},
{
x: events2.pageX,
y: events2.pageY,
},
)
}
store.originScale = store.scale || 1
}
function onTouchMove(event: TouchEvent) {
if (!state.store.moveable)
return
const store = state.store
// event.preventDefault();
const touches = event.touches
const events = touches[0]
const events2 = touches[1]
// 双指移动
if (events2) {
// 获得当前两点间的距离
const curDistance = getDistance(
{
x: events.pageX,
y: events.pageY,
},
{
x: events2.pageX,
y: events2.pageY,
},
)
/**
* 此处计算倍数,距离放大(缩小) k 倍则 scale 也 扩大(缩小) k 倍。距离放大(缩小)倍数 = 结束时两点距离 除以 开始时两点距离
* 注意此处的 scale 变化是基于 store.scale 的。
* store.scale 是一个暂存值,比如第一次放大 2 倍,则 store.scale 为 2。
* 再次两指触碰的时候store.originScale 就为 store.scale 的值,基于此时的 store.scale 继续放大缩小。 *
*/
const curScale = curDistance / store.oriDistance
store.scale = store.originScale * curScale
// 最大放大 3 倍,缩小后松手要弹回原比例
if (store.scale > 3)
store.scale = 3
scaleNow()
}
}
function onTouchEnd() {
state.lastTouchEndTime = new Date().getTime()
const store = state.store
store.moveable = false
if ((store.scale < 1.1 && store.scale > 1) || store.scale < 1) {
store.scale = 1
scaleNow()
}
}
function longPress(image: ImageInterface) {
emit('longPress', image)
}
function init() {
document.addEventListener('touchmove', onTouchMove)
document.addEventListener('touchend', onTouchEnd)
document.addEventListener('touchcancel', onTouchEnd)
}
watch(
() => props.show,
(val) => {
state.showPop = val
if (val) {
setActive(props.initNo)
// #ifdef H5
init()
// #endif
}
},
)
watch(
() => props.initNo,
(val) => {
if (val !== state.active)
setActive(val)
},
)
onMounted(() => {
setActive(props.initNo)
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-image-preview`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutPopup v-model:visible="state.showPop" :lock-scroll="true" pop-class="nut-image-preview-custom-pop">
<view :class="classes" :style="customStyle" @touchstart.capture="(onTouchStart as any)">
<NutSwiper
v-if="state.showPop"
:auto-play="autoplay"
custom-class="nut-image-preview-swiper"
:loop="isLoop"
:is-prevent-default="false"
direction="horizontal"
:init-page="initNo"
:pagination-visible="paginationVisible"
:pagination-color="paginationColor"
@change="setActive"
>
<NutSwiperItem v-for="(item, index) in images" :key="index">
<movable-area class="nut-image-movable-area">
<movable-view
:disabled="!scale"
inertia
scale-area
class="nut-image-preview-img"
:scale="scale"
direction="all"
>
<image
mode="aspectFit"
:src="item.src"
class="nut-image-preview-img"
@long-press="longPress(item)"
@long-tap="longPress(item)"
@click.stop="closeOnImg"
/>
</movable-view>
</movable-area>
</NutSwiperItem>
</NutSwiper>
</view>
<view v-if="showIndex" class="nut-image-preview-index">
{{ state.active + 1 }} / {{ images.length }}
</view>
<view
v-if="closeable"
class="nut-image-preview-close-icon"
:style="styles"
@click="onClose"
>
<NutIcon name="circle-close" custom-color="#ffffff" />
</view>
</NutPopup>
</template>
<style lang="scss">
@import './index';
</style>