init
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue'
|
||||
import { CLOSE_EVENT } from '../_constants'
|
||||
import type { Interceptor } from '../_utils'
|
||||
import { commonProps, isNumber, makeArrayProp, makeNumberProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
|
||||
import type { ImageInterface } from './types'
|
||||
|
||||
export const imagepreviewProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 是否展示预览图片
|
||||
*/
|
||||
show: Boolean,
|
||||
/**
|
||||
* @description 是否缩放图片
|
||||
*/
|
||||
scale: Boolean,
|
||||
/**
|
||||
* @description 预览图片数组
|
||||
*/
|
||||
images: makeArrayProp<ImageInterface>([]),
|
||||
/**
|
||||
* @description 点击图片可以退出预览
|
||||
*/
|
||||
contentClose: truthProp,
|
||||
/**
|
||||
* @description 初始页码
|
||||
*/
|
||||
initNo: makeNumberProp(0),
|
||||
/**
|
||||
* @description 分页指示器是否展示
|
||||
*/
|
||||
paginationVisible: Boolean,
|
||||
/**
|
||||
* @description 分页指示器选中的颜色
|
||||
*/
|
||||
paginationColor: makeStringProp('#fff'),
|
||||
/**
|
||||
* @description 自动轮播时长,0 表示不会自动轮播
|
||||
*/
|
||||
autoplay: makeNumericProp(3000),
|
||||
/**
|
||||
* @description 是否显示页码
|
||||
*/
|
||||
showIndex: truthProp,
|
||||
/**
|
||||
* 是否显示关闭图标
|
||||
* @description 是否显示关闭图标
|
||||
*/
|
||||
closeable: Boolean,
|
||||
/**
|
||||
* @description 关闭图标位置,可选值:`top-left`
|
||||
*/
|
||||
closeIconPosition: makeStringProp<'top-right' | 'top-left'>('top-right'),
|
||||
/**
|
||||
* @description 关闭前的回调函数,返回 `false` 可阻止关闭,支持返回 `Promise`
|
||||
*/
|
||||
beforeClose: Function as PropType<Interceptor>,
|
||||
/**
|
||||
* @description 是否循环播放
|
||||
*/
|
||||
isLoop: truthProp,
|
||||
}
|
||||
|
||||
export type ImagePreviewProps = ExtractPropTypes<typeof imagepreviewProps>
|
||||
|
||||
export const imagepreviewEmits = {
|
||||
[CLOSE_EVENT]: () => true,
|
||||
change: (val: number) => isNumber(val),
|
||||
longPress: (image: ImageInterface) => image instanceof Object,
|
||||
}
|
||||
|
||||
export type ImagePreviewEmits = typeof imagepreviewEmits
|
||||
277
uni_modules/nutui-uni/components/imagepreview/imagepreview.vue
Normal file
277
uni_modules/nutui-uni/components/imagepreview/imagepreview.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<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>
|
||||
93
uni_modules/nutui-uni/components/imagepreview/index.scss
Normal file
93
uni_modules/nutui-uni/components/imagepreview/index.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
@import '../popup/index';
|
||||
@import '../swiper/index';
|
||||
@import '../swiperitem/index';
|
||||
|
||||
.nut-image-movable-area{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.nut-image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&-swiper {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&-index {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 2002;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&-close-icon {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 15px;
|
||||
z-index: 2002;
|
||||
|
||||
&-right {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
&-left {
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-bg {
|
||||
background: rgb(0 0 0 / 90%);
|
||||
}
|
||||
|
||||
.popup-box {
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-image-preview-custom-pop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.nut-image-preview-swiper .nut-swiper-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
.nut-image-preview-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nut-video {
|
||||
// height: auto;
|
||||
video {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/imagepreview/index.ts
Normal file
1
uni_modules/nutui-uni/components/imagepreview/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type * from './imagepreview'
|
||||
35
uni_modules/nutui-uni/components/imagepreview/types.ts
Normal file
35
uni_modules/nutui-uni/components/imagepreview/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Interceptor } from '../_utils'
|
||||
|
||||
export interface ImageInterface {
|
||||
src: string
|
||||
}
|
||||
|
||||
export interface ImagePreviewOptions {
|
||||
show: boolean
|
||||
images: ImageInterface[]
|
||||
videos: []
|
||||
contentClose: boolean
|
||||
initNo: number
|
||||
paginationVisible: boolean
|
||||
paginationColor: string
|
||||
autoplay: [number, string]
|
||||
isWrapTeleport: boolean
|
||||
showIndex?: boolean
|
||||
closeable?: boolean
|
||||
closeIcon?: string
|
||||
closeIconPosition?: string
|
||||
beforeClose?: Interceptor
|
||||
maxZoom?: number
|
||||
minZoom?: number
|
||||
isLoop?: boolean
|
||||
close?: () => void
|
||||
change?: (index: number) => void
|
||||
}
|
||||
|
||||
export const baseProps = {
|
||||
show: { type: Boolean, default: false },
|
||||
initNo: { type: Number, default: 0 },
|
||||
showIndex: { type: Boolean, default: true },
|
||||
minZoom: { type: Number, default: 1 / 3 },
|
||||
maxZoom: { type: Number, default: 3 },
|
||||
}
|
||||
Reference in New Issue
Block a user