【uniapp+Typescript】300行代码手撸了一个多端图片比较组件

今天刚新鲜出炉的。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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/897222.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

化繁为简,使用 ADManager Plus 简化账户生命周期管理

在 IT 环境中&#xff0c;编排指的是对工作流、应用程序和系统的协调管理&#xff0c;旨在通过简化流程来优化业务性能。IT 管理员可以通过编排&#xff0c;从单个控制台自动执行一系列任务&#xff0c;例如预配账户帐户、数据库管理、事件处理、应用程序和云资源管理。编排对支…

第23章 - Elasticsearch 洞悉你的查询:如何在上线前发现潜在问题!

文章目录 1. 前言2. Profile API - 查询优化2.1 Profile API 简单介绍2.2 查询结果图形化2.3 Profile 注意事项 3. Explain API - 解释查询 1. 前言 在第 21 章中&#xff0c;我介绍了 Elasticsearch 的读优化&#xff0c;但你是否曾疑惑&#xff1a;如何在上线前判断查询的耗…

springboot基于Java的民宿山庄农家乐系统设计与实现

文章目录 前言项目介绍技术介绍功能介绍核心代码数据库参考 系统效果图文章目录 前言 文章底部名片&#xff0c;获取项目的完整演示视频&#xff0c;免费解答技术疑问 项目介绍 当今社会已经步入了科学技术进步和经济社会快速发展的新时期&#xff0c;国际信息和学术交流也不…

Mojo在Windows上详细安装步骤

Mojo官方文档是基于Linux写的&#xff0c;在Windows上基于WSLUbuntu安装还有些细节问题需要注意&#xff0c;完整安装步骤整理如下&#xff1a; 1.Windows版本必须是Windows10以上&#xff0c;而且版本≥1903&#xff0c;或者内部版本≥18362&#xff0c;若不满足&#xff0c;…

渗透测试实战—教育攻防演练中突破网络隔离

免责声明&#xff1a;文章来源于真实渗透测试&#xff0c;已获得授权&#xff0c;且关键信息已经打码处理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本…

【redis】热点key问题

【redis】热点key问题 【一】什么是热点key问题【二】什么样的key被称为热key【三】热点Key问题的危害【四】如何监控发现热点key【五】热点Key的解决方案【1】使用二级缓存【2】将热key分散到不同的服务器中【3】热key拆分【4】将核心/非核心业务做Redis的隔离 【六】业界已有…

C语言指针(1)

指针一句话就是存储地址的一个变量&#xff0c;当你想要拿到一个地址就需要用到&运算符。 如果要拿数组的地址就不用&运算符&#xff0c;因为数组名就是数组首元素的地址。 int main() {int pa0;int* p&pa;int arr[3]{1,2,3};int* qarr;printf("%d",*p)…

【linux】线程 (三)

13. 常见锁概念 &#xff08;一&#xff09;了解死锁 死锁是指在一组进程中的各个进程均占有不会释放的资源&#xff0c;但因互相申请被其他进程占有的&#xff0c;且不释放的资源&#xff0c;而处于的一种永久等待状态 &#xff08;二&#xff09;死锁四个必要条件 互斥条件…

基准线markLine的值超过坐标轴范围导致markline不显示

解决问题&#xff1a;动态设置yAxis的max值&#xff08;解决基准线不在y轴范围&#xff09; yAxis: [{name: 单位&#xff1a;千,...yAxis,nameTextStyle:{...yAxis.nameTextStyle,padding: [0,26,0,24]},paddingLeft:24,paddingRight:26},{name: 单位&#xff1a;百分比,...yA…

Java开发中知识点整理

正则表达式 测试网址 List<?> List<List<Object>> dataList (List<List<Object>>) httpResponseBody.getData();for (List<Object> data : dataList) {DataSourceEntity dataSource new DataSourceEntity(dataSourceEntity);dataSou…

【二刷hot100】day 4

终于有时间刷刷力扣&#xff0c;求实习中。。。。 目录 1.最大子数组和 2.合并区间 3.轮转数组 4.除自身以外数组的乘积 1.最大子数组和 class Solution {public int maxSubArray(int[] nums) {//就是说可以转换为计算左边的最大值&#xff0c;加上中间的值&#xff0c…

Git的原理和使用(六)

本文主要讲解企业级开发模型 1. 引入 交付软件的流程&#xff1a;开发->测试->发布上线 上面三个过程可以详细划分为一下过程&#xff1a;规划、编码、构建、测试、发 布、部署和维护 最初&#xff0c;程序⽐较简单&#xff0c;⼯作量不⼤&#xff0c;程序员⼀个⼈可以完…

一文详解“位运算“在算法中的应用

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; 优选算法专题 目录 位运算的相关介绍&#xff08;重要&#xff09; 136. 只出现一次的数字 191.位1的个数 461. 汉明距离 260. 只出现一…

大模型的经典面试问题及答案,非常详细收藏我这一篇就够了

大语言模型&#xff08;LLM&#xff09;在人工智能中变得越来越重要&#xff0c;在各个行业都有应用。随着对大语言模型专业人才需求的增长&#xff0c;本文提供了一套全面的面试问题和答案&#xff0c;涵盖了基本概念、先进技术和实际应用。如果你正在为面试做准备&#xff0c…

【优选算法篇】在分割中追寻秩序:二分查找的智慧轨迹

文章目录 C 二分查找详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;热身练习1.1 二分查找基本实现解题思路图解分析C代码实现易错点提示代码解读 1.2 在排序数组中查找元素的第一个和最后一个位置解题思路1.2.1 查找左边界算法步骤&#xff1a;图解分析C代码实现 1…

国产大模型基础能力大比拼 - 计数:通义千文 vs 文心一言 vs 智谱 vs 讯飞-正经应用场景的 LLM 逻辑测试

在大语言模型&#xff08;LLM&#xff09;不断涌现的时代&#xff0c;如何评估这些国产大模型的逻辑推理能力&#xff0c;尤其是在处理基础计数问题上的表现&#xff0c;成为了一个备受关注的话题。随着越来越多的国产大模型进入市场&#xff0c;比较它们在不同任务中的表现尤为…

群晖通过 Docker 安装 Gitea

1. 准备工作 1.1 安装 docker 套件 2.2 安装MySQL&#xff08;可选&#xff09; 群晖通过 Docker 安装 MySQL-CSDN博客 如果安装了MySQL&#xff0c;可以创建gitea用户和gitea_db数据库&#xff0c;并且赋予权限 #创建数据库 CREATE DATABASE gitea_db; #确认数据库已创建…

【Jenkins】2024 最新版本的 Jenkins 权限修改为 root 用户启动,解决 permission-denied 报错问题

最新版本的 Jenkins 修改 /etc/sysconfig/jenkins 中的 JENKINS_USERroot不会再生效&#xff0c;需要按照以下配置进行操作&#xff1a; vim /usr/lib/systemd/system/jenkins.service然后重启就可以了 systemctl daemon-reload # 重新加载 systemd 的配置文件 systemctl res…

JavaEE 多线程第三节 (lambda方法实现多线程/Thread属性和方法/前台线程后台线程)

欢迎阅读前序课程JavaEE 多线程第二节 (多线程的简单实现Thread/Runable)-CSDN博客 1. lambda方法实现多线程 public class Test {public static void main(String[] args) throws InterruptedException {Thread t new Thread(()->{while (true){System.out.println("…

探索云边缘与边缘云:技术革新与应用前景

#1024程序员节&#xff5c;征文# 在当今数字化快速发展的时代&#xff0c;云计算已经成为了企业和个人处理和存储数据的重要手段。然而&#xff0c;随着物联网&#xff08;IoT&#xff09;设备的爆炸式增长以及对低延迟、高带宽和实时处理的需求不断增加&#xff0c;云边缘和边…