今天刚新鲜出炉的。DCloud市场上的看了下,都不好用,于是自己撸了个。基于unibest+ccframe框架。
用户图片对比,支持滑块拖动对比、图片放大缩小、下载这些基本功能。
左右对比模式还没写,等什么时候想弄了,再来更新。
效果图:
放大
平移
下载
由于这个是对比工具,要求前后两个图片比例一致,否则会变形,大小可以不一致。
<!--
ccframe slider图像比较组件:
1)拖动比较图片区域
2)放大和平移对比图片工具
3)目标图片下载
4)支持滑块比较和左右比较两种模式(未完成)
要求前后两个图片比例一致,否则会变形
-->
<template>
<view
class="x-items-left"
:style="{ width: props.width + 'rpx', height: props.height + 'rpx' }"
v-bind="$attrs"
>
<view
class="flex-1 border-1px border-solid relative compare-img-area"
:style="{ borderColor: props.borderColor }"
@touchstart="moveSlider"
@touchmove="moveSlider"
>
<view
class="relative overflow-hidden h-full inline-block"
:style="{ width: data.sliderLeft + 'px' }"
>
<image
class="absolute"
:style="{
width: data.zoomWidth + 'px',
height: data.zoomHeight + 'px',
left: data.zoomLeft + 'px',
top: data.zoomTop + 'px'
}"
:src="props.imgBefore"
/>
</view>
<view class="relative w-2px h-full top-0 bg-white inline-block z-2" ref="slider">
<image src="@img/ccframe/imgcomp-slider.svg" class="absolute-center w-30px h-30px" />
</view>
<view
class="relative h-full overflow-hidden inline-block"
:style="{ width: `${data.areaWidth - data.sliderLeft - 2}px` }"
>
<image
class="absolute"
:style="{
width: data.zoomWidth + 'px',
height: data.zoomHeight + 'px',
left: data.zoomLeft - data.sliderLeft - 2 + 'px',
top: data.zoomTop + 'px'
}"
:src="props.imgAfter"
/>
</view>
</view>
<view class="w-90rpx">
<view
class="w-45rpx h-45rpx flex-center text-ccwidgets text-35rpx ml-20rpx rotate-90"
:style="{ color: props.btnColor, borderColor: props.borderColor }"
@click="panImg(0, 50)"
>
{{ '\ue656' }}
</view>
<view
class="w-45rpx h-45rpx flex-center text-ccwidgets text-35rpx ml-20rpx mt-15rpx"
:style="{ color: props.btnColor, borderColor: props.borderColor }"
@click="panImg(50, 0)"
>
{{ '\ue656' }}
</view>
<view
class="w-45rpx h-45rpx flex-center text-ccwidgets text-35rpx ml-20rpx rotate-180 mt-15rpx"
:style="{ color: props.btnColor, borderColor: props.borderColor }"
@click="panImg(-50, 0)"
>
{{ '\ue656' }}
</view>
<view
class="w-45rpx h-45rpx flex-center text-ccwidgets text-35rpx ml-20rpx rotate-270 mt-15rpx"
:style="{ color: props.btnColor, borderColor: props.borderColor }"
@click="panImg(0, -50)"
>
{{ '\ue656' }}
</view>
<view
class="w-45rpx h-45rpx flex-center text-ccwidgets text-35rpx ml-20rpx mt-15rpx"
:style="{ color: props.btnColor, borderColor: props.borderColor }"
@click="zoomImg(1.5)"
>
{{ '\ue637' }}
</view>
<view
class="w-45rpx h-45rpx flex-center text-ccwidgets text-35rpx ml-20rpx mt-15rpx"
:style="{ color: props.btnColor, borderColor: props.borderColor }"
@click="zoomImg(0.66)"
>
{{ '\ue638' }}
</view>
<view
class="w-45rpx h-45rpx flex-center text-ccwidgets text-35rpx ml-20rpx mt-15rpx"
:style="{ color: props.btnColor, borderColor: props.borderColor }"
@click="saveLocal"
>
{{ '\ue622' }}
</view>
</view>
<a ref="downloadLink" class="hidden" />
</view>
</template>
<script setup lang="ts">
import * as utils from '@/utils'
// #ifdef H5
const downloadLink = ref<HTMLAnchorElement | null>(null)
// #endif
const { screenWidth } = uni.getSystemInfoSync()
const pixelUnit = screenWidth / 750 // rpx->px
console.log('pixelUnit===', pixelUnit)
let instance
const props = withDefaults(
defineProps<{
imgBefore: string // 前图片
imgAfter: string // 后图片
compareMode?: 'slider' | 'compare' // 对比模式:滑块(拖动滑块) vs 左右(拖动图片)
width: number // 组件宽度rpx
height: number // 组件高度rpx
btnColor?: string // 按钮颜色
borderColor?: string // 边框颜色
}>(),
{
compareMode: 'slider',
btnColor: 'var(--primary-color)',
borderColor: '#888'
}
)
const data = reactive<{
areaWidth?: number
areaHeight?: number
imgWidth?: number
imgHeight?: number
zoomWidth: number // 缩放后的宽度
zoomHeight: number // 缩放后的高度
zoom?: number // 缩放比
maxZoomRatio?: number // 缩放到最小限制
zoomLeft?: number // 缩放后左边的位置
zoomTop?: number // 缩放后顶部的位置
sliderLeft: number // 滑块左边位置
clientLeft?: number // 拖动区域左边屏幕位置
}>({
areaWidth: 200,
areaHeight: 200,
zoomWidth: 200,
zoomHeight: 200,
sliderLeft: 100
})
onMounted(async () => {
instance = getCurrentInstance()
uni.getImageInfo({
src: props.imgAfter,
success: async (result: UniNamespace.GetImageInfoSuccessData) => {
if (result.width > 0 && result.height > 0) {
data.imgWidth = result.width // 边框2px
data.imgHeight = result.height
const { width, height, left } = await utils.getRect('.compare-img-area', instance)
data.areaWidth = width - 2 // 去掉边框
data.areaHeight = height - 2
data.clientLeft = left
const imgAspectRatio = data.imgWidth / data.imgHeight
const areaAspectRatio = data.areaWidth / data.areaHeight
if (imgAspectRatio > areaAspectRatio) {
data.zoom = data.areaWidth / data.imgWidth
data.zoomWidth = data.areaWidth
data.zoomHeight = data.imgHeight * data.zoom
} else {
data.zoom = data.areaHeight / data.imgHeight
data.zoomWidth = data.imgWidth * data.zoom
data.zoomHeight = data.areaHeight
}
data.maxZoomRatio = data.zoom
data.zoomLeft = (data.areaWidth - data.zoomWidth) / 2
data.zoomTop = (data.areaHeight - data.zoomHeight) / 2
data.sliderLeft = data.areaWidth / 2 - 1
}
}
})
})
const moveSlider = (e: TouchEvent) => {
const pos = e.touches[0].clientX - data.clientLeft
if (pos <= 1) data.sliderLeft = 1
else if (pos >= data.areaWidth) data.sliderLeft = data.areaWidth - 3
else data.sliderLeft = pos
}
/**
* 缩放图片
* @param scale 2是放大0.5是缩小一半
*/
const zoomImg = (scale: number) => {
let newZoom = data.zoom * scale
newZoom = Math.max(data.maxZoomRatio, Math.min(newZoom, 2))
const currentZoomWidth = data.imgWidth * data.zoom
const currentZoomHeight = data.imgHeight * data.zoom
const centerX = data.zoomLeft + currentZoomWidth / 2
const centerY = data.zoomTop + currentZoomHeight / 2
data.zoomWidth = data.imgWidth * newZoom
data.zoomHeight = data.imgHeight * newZoom
data.zoomLeft = centerX - data.zoomWidth / 2
data.zoomTop = centerY - data.zoomHeight / 2
data.zoom = newZoom
if (data.zoomWidth <= data.areaWidth) {
data.zoomLeft = (data.areaWidth - data.zoomWidth) / 2
} else {
if (data.zoomLeft > 0) {
data.zoomLeft = 0
}
if (data.zoomLeft + data.zoomWidth < data.areaWidth) {
data.zoomLeft = data.areaWidth - data.zoomWidth
}
}
if (data.zoomHeight <= data.areaHeight) {
data.zoomTop = (data.areaHeight - data.zoomHeight) / 2
} else {
if (data.zoomTop > 0) {
data.zoomTop = 0
}
if (data.zoomTop + data.zoomHeight < data.areaHeight) {
data.zoomTop = data.areaHeight - data.zoomHeight
}
}
}
const panImg = (deltaX: number, deltaY: number) => {
let newZoomLeft = data.zoomLeft + deltaX
let newZoomTop = data.zoomTop + deltaY
if (data.zoomWidth <= data.areaWidth) {
newZoomLeft = (data.areaWidth - data.zoomWidth) / 2
} else {
newZoomLeft = Math.min(0, Math.max(newZoomLeft, data.areaWidth - data.zoomWidth))
}
if (data.zoomHeight <= data.areaHeight) {
newZoomTop = (data.areaHeight - data.zoomHeight) / 2
} else {
newZoomTop = Math.min(0, Math.max(newZoomTop, data.areaHeight - data.zoomHeight))
}
data.zoomLeft = newZoomLeft
data.zoomTop = newZoomTop
}
let blobUrl: string | null = null
const saveLocal = async () => {
// #ifdef H5
// H5直接下载,图片系统可能要开跨域
utils.alert('开始下载')
try {
const response = await fetch(props.imgAfter)
const blob = await response.blob()
blobUrl = URL.createObjectURL(blob)
const fileName = props.imgAfter.substring(props.imgAfter.lastIndexOf('/') + 1)
if (downloadLink.value) {
downloadLink.value.href = blobUrl
downloadLink.value.download = fileName
downloadLink.value.click()
}
} catch (error) {
console.error('Failed to load the file.', error)
}
// #endif
// #ifndef H5
// 其它的保存到相册
uni.downloadFile({
// 下载
url: props.imgAfter, // 图片下载地址
success: (res) => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
// 保存图片到系统相册。
filePath: res.tempFilePath, // 图片文件路径
success: function () {
utils.alert('图片保存成功')
},
fail: function (e) {
console.log(e)
utils.alert('图片保存失败')
}
})
}
}
})
// #endif
}
onUnmounted(() => {
if (blobUrl) {
URL.revokeObjectURL(blobUrl)
}
})
</script>
<style lang="scss" scoped>
.text-ccwidgets {
width: 60rpx;
height: 60rpx;
border-style: solid;
border-width: 2rpx;
border-radius: 5rpx;
&:active {
opacity: 0.9;
}
}
</style>