uniapp/uniappx实现原生图片编辑涂鸦、贴图、滤镜、旋转、裁剪等

UNIAPP/uniappx图片编辑器插件(fz-image-editor)是基于 UTS 原生插件开发的图片编辑器支持uniapp和uniappx安卓端,其功能支持裁剪、旋转、滤镜、文字、贴纸、涂鸦等丰富功能,提供完整原生编辑 UI 界面。效果如下:

功能特性如下:

  • 图片裁剪(自由/比例裁剪)

  • 图片旋转与翻转

  • 滤镜效果(多种风格)

  • 文字添加

  • 贴纸添加(内置多套贴纸素材)

  • 手绘涂鸦

  • 马赛克/模糊处理

  • 亮度/对比度调节

  • 一键撤销/重做

  • 编辑完成后回调返回新图片路径

将 `fz-image-editor` 目录复制到项目的 `uni_modules` 目录下,目录结构如下:

Android 权限说明

插件已在 `AndroidManifest.xml` 中自动声明以下权限,无需手动配置:

| `WRITE_EXTERNAL_STORAGE` | 保存编辑后的图片

| `READ_MEDIA_IMAGES` | 读取图库图片(Android 11+)

| `READ_MEDIA_VIDEO` | 读取媒体文件(Android 11+)

使用方法示例:

首先UNIAPP插件下载图片编辑器:uniapp图片编辑器插件安装到UNIAPP或你UNIAPPX项目里:

html 复制代码
import { ImageEditorUI, EditImageOptions, EditResult } from '@/uni_modules/fz-image-editor'

// 自定义输出路径(需确保目录存在)
const outputDir = uni.env.USER_DATA_PATH + '/my_edits/'
const outputPath = outputDir + 'result_' + Date.now() + '.jpg'

ImageEditorUI.openEditor(
  {
    imagePath: '/sdcard/DCIM/test.jpg',
    outputPath: outputPath
  },
  (result : EditResult) => {
    if (result.isEdited) {
      console.log('保存到自定义路径:', result.path)
    }
  }
)

uvue 示例代码如下:

html 复制代码
<template>
  <!-- #ifdef APP -->
  <scroll-view style="flex: 1;">
  <!-- #endif -->
    <view class="container">
      <!-- 选择图片区域 -->
      <view v-if="imagePath == ''" class="upload-area" @click="chooseImage">
        <text class="upload-icon">+</text>
        <text class="upload-text">点击选择图片</text>
      </view>

      <!-- 图片预览区域 -->
      <view v-if="imagePath != ''" class="preview-area">
        <image class="preview-image" :src="imagePath" mode="aspectFit"></image>
      </view>

      <!-- 操作按钮 -->
      <view class="btn-group">
        <button v-if="imagePath != ''" class="btn-primary" @click="openEditor">
          开始编辑
        </button>
        <button v-if="resultPath != ''" class="btn-success" @click="saveToAlbum">
          保存到相册
        </button>
      </view>

      <!-- 编辑结果展示 -->
      <view v-if="resultPath != ''" class="result-area">
        <text class="result-label">编辑完成</text>
        <image class="result-image" :src="resultPath" mode="aspectFit"></image>
        <text class="result-path">保存路径:{{ resultPath }}</text>
      </view>
    </view>
  <!-- #ifdef APP -->
  </scroll-view>
  <!-- #endif -->
</template>

<script lang="uts">
  import { ImageEditorUI, EditImageOptions, EditResult } from '@/uni_modules/fz-image-editor'

  export default {
    data() {
      return {
        imagePath: '' as string,
        resultPath: '' as string
      }
    },
    methods: {
      // 选择本地图片
      chooseImage() {
        uni.chooseImage({
          count: 1,
          sourceType: ['album', 'camera'],
          success: (res) => {
            // tempFilePaths 返回的是 file:// 开头的路径
            // 需要去除 file:// 前缀,使用绝对路径
            let tempPath = res.tempFilePaths[0]
            if (tempPath.startsWith('file://')) {
              tempPath = tempPath.replace('file://', '')
            }
            this.imagePath = tempPath
            this.resultPath = ''
          },
          fail: (err) => {
            console.log('选择图片失败:', err)
          }
        })
      },

      // 打开图片编辑器
      openEditor() {
        if (this.imagePath == '') {
          uni.showToast({ title: '请先选择图片', icon: 'none' })
          return
        }

        const options : EditImageOptions = {
          imagePath: this.imagePath,
          // outputPath 可选,不传时自动生成到缓存目录
          // outputPath: uni.env.USER_DATA_PATH + '/edited_' + Date.now() + '.jpg'
        }

        ImageEditorUI.openEditor(options, (result : EditResult) => {
          if (result.isEdited) {
            console.log('编辑成功,路径:', result.path)
            this.resultPath = result.path
            this.imagePath = result.path
            uni.showToast({ title: '编辑成功', icon: 'success' })
          } else {
            console.log('用户取消编辑')
            uni.showToast({ title: '已取消编辑', icon: 'none' })
          }
        })
      },

      // 保存到相册
      saveToAlbum() {
        if (this.resultPath == '') return
        uni.saveImageToPhotosAlbum({
          filePath: this.resultPath,
          success: () => {
            uni.showToast({ title: '已保存到相册', icon: 'success' })
          },
          fail: (err) => {
            uni.showToast({ title: '保存失败:' + err.errMsg, icon: 'none' })
          }
        })
      }
    }
  }
</script>

<style>
  .container {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px;
  }

  .upload-area {
    width: 200px;
    height: 200px;
    border: 2px dashed #cccccc;
    border-radius: 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    margin-top: 40px;
  }

  .upload-icon {
    font-size: 48px;
    color: #999999;
    line-height: 1;
  }

  .upload-text {
    font-size: 14px;
    color: #999999;
    margin-top: 8px;
  }

  .preview-area {
    width: 100%;
    margin-top: 16px;
  }

  .preview-image {
    width: 100%;
    height: 300px;
    border-radius: 8px;
  }

  .btn-group {
    display: flex;
    flex-direction: row;
    margin-top: 20px;
    gap: 12px;
  }

  .btn-primary {
    background-color: #7B2FBE;
    color: #ffffff;
    border-radius: 24px;
    font-size: 16px;
    padding: 12px 32px;
    border: none;
  }

  .btn-success {
    background-color: #0e932e;
    color: #ffffff;
    border-radius: 24px;
    font-size: 16px;
    padding: 12px 32px;
    border: none;
  }

  .result-area {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 24px;
    width: 100%;
  }

  .result-label {
    font-size: 16px;
    font-weight: bold;
    color: #0e932e;
  }

  .result-image {
    width: 100%;
    height: 300px;
    border-radius: 8px;
    margin-top: 8px;
  }

  .result-path {
    font-size: 12px;
    color: #666666;
    margin-top: 8px;
    word-break: break-all;
  }
</style>

vue示例代码如下:

html 复制代码
<template>
  <!-- #ifdef APP -->
  <scroll-view style="flex: 1;">
  <!-- #endif -->
    <view class="page-wrap">

      <view v-if="imagePath == ''" class="picker-box" @click="pickImage">
        <text class="picker-plus">+</text>
        <text class="picker-hint">点击选择或拍摄图片</text>
      </view>

      <view v-if="imagePath != ''" class="img-box">
        <image class="img-preview" :src="imagePath" mode="aspectFit" />
        <text class="img-tip">点击下方按钮开始编辑</text>
      </view>

      <view class="action-row">
        <view class="btn btn-purple" @click="pickImage">
          <text class="btn-text">选择图片</text>
        </view>
        <view v-if="imagePath != ''" class="btn btn-blue" @click="startEdit">
          <text class="btn-text">编辑图片</text>
        </view>
      </view>

      <view v-if="resultPath != ''" class="result-card">
        <text class="result-title">编辑结果</text>
        <image class="result-img" :src="resultPath" mode="aspectFit" />
        <view class="result-actions">
          <view class="btn btn-green" @click="doSave">
            <text class="btn-text">保存相册</text>
          </view>
          <view class="btn btn-orange" @click="doShare">
            <text class="btn-text">分享图片</text>
          </view>
        </view>
      </view>

    </view>
  <!-- #ifdef APP -->
  </scroll-view>
  <!-- #endif -->
</template>

<script>
  import { ImageEditorUI } from '@/uni_modules/fz-image-editor'

  export default {
    data() {
      return {
        imagePath: '',
        resultPath: ''
      }
    },
    methods: {
      pickImage() {
        uni.chooseImage({
          count: 1,
          sourceType: ['album', 'camera'],
          success: (res) => {
            let p = res.tempFilePaths[0]
            // 去除 file:// 协议前缀,保留绝对路径
            if (p.startsWith('file://')) {
              p = p.replace('file://', '')
            }
            this.imagePath = p
            this.resultPath = ''
          }
        })
      },

      startEdit() {
        if (this.imagePath == '') {
          uni.showToast({ title: '请先选择图片', icon: 'none' })
          return
        }
        ImageEditorUI.openEditor(
          { imagePath: this.imagePath },
          (result) => {
            if (result.isEdited) {
              this.resultPath = result.path
              this.imagePath = result.path
              uni.showToast({ title: '编辑完成', icon: 'success' })
            } else {
              uni.showToast({ title: '已取消', icon: 'none' })
            }
          }
        )
      },

      doSave() {
        uni.saveImageToPhotosAlbum({
          filePath: this.resultPath,
          success: () => uni.showToast({ title: '保存成功', icon: 'success' }),
          fail: () => uni.showToast({ title: '保存失败', icon: 'error' })
        })
      },

      doShare() {
        uni.shareWithSystem({
          imageUrl: this.resultPath,
          type: 'image',
          success(res) { console.log('分享成功', res) },
          fail(err) { console.log('分享失败', err) }
        })
      }
    }
  }
</script>

<style>
  .page-wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 24px 16px;
  }

  .picker-box {
    width: 180px;
    height: 180px;
    border: 2px dashed #bbbbbb;
    border-radius: 16px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    margin-top: 30px;
  }

  .picker-plus {
    font-size: 52px;
    color: #aaaaaa;
    line-height: 1;
  }

  .picker-hint {
    font-size: 13px;
    color: #aaaaaa;
    margin-top: 6px;
  }

  .img-box {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 16px;
  }

  .img-preview {
    width: 100%;
    height: 260px;
    border-radius: 10px;
  }

  .img-tip {
    font-size: 13px;
    color: #888888;
    margin-top: 8px;
  }

  .action-row {
    display: flex;
    flex-direction: row;
    justify-content: center;
    margin-top: 20px;
    gap: 16px;
  }

  .btn {
    padding: 12px 28px;
    border-radius: 28px;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
  }

  .btn-text {
    color: #ffffff;
    font-size: 15px;
  }

  .btn-purple {
    background-color: #7B2FBE;
  }

  .btn-blue {
    background-color: #1677ff;
  }

  .btn-green {
    background-color: #0e932e;
  }

  .btn-orange {
    background-color: #fa8c16;
  }

  .result-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 28px;
    width: 100%;
    background-color: #f6fff8;
    border-radius: 12px;
    padding: 16px;
    border: 1px solid #d9f7be;
  }

  .result-title {
    font-size: 16px;
    font-weight: bold;
    color: #0e932e;
  }

  .result-img {
    width: 100%;
    height: 260px;
    border-radius: 8px;
    margin-top: 12px;
  }

  .result-actions {
    display: flex;
    flex-direction: row;
    justify-content: center;
    margin-top: 14px;
    gap: 16px;
  }
</style>
相关推荐
计算机学姐1 天前
基于微信小程序的校园失物招领管理系统【uniapp+springboot+vue】
java·vue.js·spring boot·mysql·信息可视化·微信小程序·uni-app
2501_915921431 天前
HTTPS前端劫持 新一代流量劫持解决方案
前端·网络协议·ios·小程序·https·uni-app·iphone
爱怪笑的小杰杰1 天前
优化 UniApp 日历组件的多语言切换:告别 setLocale 引起的 App 重启
java·前端·uni-app
计算机学姐1 天前
基于微信小程序的宠物服务系统【uniapp+springboot+vue】
java·vue.js·spring boot·mysql·微信小程序·uni-app·宠物
2501_915909061 天前
iOS应用签名的三种方法全解析:从官方到第三方工具
android·ios·小程序·https·uni-app·iphone·webview
心中无石马2 天前
uniapp引入tailwindcss4.x
前端·css·uni-app
fix一个write十个2 天前
【uniApp开发】微信小程序 web-view 内嵌 H5 跳转支付踩坑实录
微信小程序·uni-app
wuxianda10302 天前
苹果App上架4.3a被拒解决方案汇报总结
ios·uni-app·objective-c·cocoa·苹果上架·4.3a
西洼工作室2 天前
uniapp+vue3+python对接阿里云短信认证服务alibabacloud_dypnsapi20170525
python·阿里云·uni-app