Skip to content

快速开始

简介

vue-select-avatar 是一个基于 Vue3 的头像选择的库,它提供了一个头像选择器组件、一个预览组件和一些工具函数。相比上一个版本,仅提供一个函数,虽然更加便捷,但是自定义和扩展难度更高,现在这个版本仅提供核心组件/工具函数,它更加精简、低耦合。同时也在快速使用中提供使用案例,以供参考,可自行复制。

本库使用三个配置对象,分别对应选择图片、选择图片截取位置、图片截取:

安装

bash
npm i vue-select-avatar
bash
pnpm add vue-select-avatar
bash
yarn add vue-select-avatar

WARNING

3.0+ 版本仅支持 Vue 3.x,Vue 2.x 用户请使用 vue-select-avatar@2.x

使用

基础使用

查看代码
vue
<script setup lang="ts">
import 'vue-select-avatar/style.css' // 引入样式
import { Viewport, isCancelError, getErrorMessage /* ... */ } from 'vue-select-avatar' // 引入组件/函数等

import { ref } from 'vue'
import { ElMessage } from 'element-plus'

const viewportRef = ref<InstanceType<typeof Viewport>>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)

const handleSelect = () => {
  viewportRef.value?.select({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
    if (isCancelError(err)) return
    // 错误处理
    console.error(err)
    ElMessage.error(getErrorMessage(err))
  })
}

const handleCropper = async () => {
  try {
    const file = await viewportRef.value?.cropper<File>()
    if (file) {
      if (src.value) {
        URL.revokeObjectURL(src.value)
      }
      src.value = URL.createObjectURL(file)
      fileSize.value = file.size
    }
  } catch (error) {
    // 错误处理
    console.error(error)
    ElMessage.error(getErrorMessage(error))
  }
}

const handleClear = () => {
  src.value = ''
}

const handleLoad = (e: Event) => {
  size.value = (e.target as HTMLImageElement).naturalWidth
}

// 辅助函数
const formatBytes = (bytes: number, decimals = 2) => {
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + units[i] + 'B'
}
</script>

<template>
  <div style="width: fit-content">
    <div style="display: flex; justify-content: space-between">
      <button @click="handleSelect">选择图片</button>
      <button @click="handleCropper">截取</button>
    </div>
    <Viewport ref="viewportRef" grid />
  </div>
  <template v-if="src">
    <div style="font-size: 13px">{{ `${size}x${size} ${formatBytes(fileSize)}` }}</div>
    <img :src="src" @load="handleLoad" />
    <button @click="handleClear">清除</button>
  </template>
</template>

配合预览组件

查看代码
vue
<script setup lang="ts">
import 'vue-select-avatar/style.css' // 引入样式
import { Viewport, Preview, isCancelError, getErrorMessage } from 'vue-select-avatar' // 引入组件/函数等

import { ref } from 'vue'
import { ElMessage } from 'element-plus'

const viewportRef = ref<InstanceType<typeof Viewport>>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)

const handleSelect = () => {
  viewportRef.value?.select({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
    if (isCancelError(err)) return
    // 错误处理
    console.error(err)
    ElMessage.error(getErrorMessage(err))
  })
}

const handleCropper = async () => {
  try {
    const file = await viewportRef.value?.cropper<File>()
    if (file) {
      if (src.value) {
        URL.revokeObjectURL(src.value)
      }
      src.value = URL.createObjectURL(file)
      fileSize.value = file.size
    }
  } catch (error) {
    // 错误处理
    console.error(error)
    ElMessage.error(getErrorMessage(error))
  }
}

const handleClear = () => {
  src.value = ''
}

const handleLoad = (e: Event) => {
  size.value = (e.target as HTMLImageElement).naturalWidth
}

// 辅助函数
const formatBytes = (bytes: number, decimals = 2) => {
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + units[i] + 'B'
}
</script>

<template>
  <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap">
    <div style="width: fit-content">
      <div style="display: flex; justify-content: space-between">
        <button @click="handleSelect">选择图片</button>
        <button @click="handleCropper">截取</button>
      </div>
      <Viewport ref="viewportRef" grid />
    </div>
    <Preview :viewport-ref="viewportRef" bg="#252526" />
  </div>
  <template v-if="src">
    <div style="font-size: 13px">{{ `${size}x${size} ${formatBytes(fileSize)}` }}</div>
    <img :src="src" @load="handleLoad" />
    <button @click="handleClear">清除</button>
  </template>
</template>

图片固定模式

开启此模式后鼠标/触摸控制的就是观察窗,而不是图片。

查看代码
vue
<script setup lang="ts">
import 'vue-select-avatar/style.css' // 引入样式
import { getErrorMessage, isCancelError, Viewport /* ... */ } from 'vue-select-avatar' // 引入组件/函数等

import { ref } from 'vue'
import { ElMessage } from 'element-plus'

const viewportRef = ref<InstanceType<typeof Viewport>>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)

const handleSelect = () => {
  viewportRef.value?.select({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
    if (isCancelError(err)) return
    // 错误处理
    console.error(err)
    ElMessage.error(getErrorMessage(err))
  })
}

const handleCropper = async () => {
  try {
    const file = await viewportRef.value?.cropper<File>()
    if (file) {
      if (src.value) {
        URL.revokeObjectURL(src.value)
      }
      src.value = URL.createObjectURL(file)
      fileSize.value = file.size
    }
  } catch (error) {
    // 错误处理
    console.error(error)
    ElMessage.error(getErrorMessage(error))
  }
}

const handleClear = () => {
  src.value = ''
}

const handleLoad = (e: Event) => {
  size.value = (e.target as HTMLImageElement).naturalWidth
}

// 辅助函数
const formatBytes = (bytes: number, decimals = 2) => {
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + units[i] + 'B'
}
</script>

<template>
  <div style="width: fit-content">
    <div style="display: flex; justify-content: space-between">
      <button @click="handleSelect">选择图片</button>
      <button @click="handleCropper">截取</button>
    </div>
    <Viewport ref="viewportRef" grid fixed-image />
  </div>
  <template v-if="src">
    <div style="font-size: 13px">{{ `${size}x${size} ${formatBytes(fileSize)}` }}</div>
    <img :src="src" @load="handleLoad" />
    <button @click="handleClear">清除</button>
  </template>
</template>

先选择图片再截取

使用 selectImage 选择完图片后,再通过 info props 传入 Viewport 组件

查看代码
vue
<script setup lang="ts">
import 'vue-select-avatar/style.css' // 引入样式
import {
  Viewport,
  getErrorMessage,
  isCancelError,
  selectImage,
  type ImageInfo,
} from 'vue-select-avatar' // 引入组件/函数等

import { ref } from 'vue'
import { ElMessage } from 'element-plus'

const viewportRef = ref<InstanceType<typeof Viewport>>()
const info = ref<ImageInfo>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)

const handleSelect = async () => {
  const res = await selectImage({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
    if (isCancelError(err)) return
    // 错误处理
    console.error(err)
    ElMessage.error(getErrorMessage(err))
  })
  if (res) {
    info.value = res
  }
}

const handleCropper = async () => {
  try {
    const file = await viewportRef.value?.cropper<File>()
    if (file) {
      if (src.value) {
        URL.revokeObjectURL(src.value)
      }
      src.value = URL.createObjectURL(file)
      fileSize.value = file.size
    }
  } catch (error) {
    // 错误处理
    console.error(error)
    ElMessage.error(getErrorMessage(error))
  }
}

const handleClear = () => {
  src.value = ''
}

const handleLoad = (e: Event) => {
  size.value = (e.target as HTMLImageElement).naturalWidth
}

const handleClearInfo = () => {
  info.value = void 0
}

// 辅助函数
const formatBytes = (bytes: number, decimals = 2) => {
  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + units[i] + 'B'
}
</script>

<template>
  <template v-if="info">
    <div style="width: fit-content">
      <div style="display: flex; justify-content: flex-end">
        <button @click="handleCropper">截取</button>
      </div>
      <Viewport ref="viewportRef" grid :info="info" />
    </div>
    <button @click="handleClearInfo">清除图片信息</button>
  </template>
  <button @click="handleSelect" v-else>选择图片</button>
  <template v-if="src">
    <div style="font-size: 13px">{{ `${size}x${size} ${formatBytes(fileSize)}` }}</div>
    <img :src="src" @load="handleLoad" />
    <button @click="handleClear">清除</button>
  </template>
</template>

Props

Viewport Props

Preview Props

Slots

Viewport Slots

插槽名说明参数
point-top-left图片固定模式下左上角的点-
point-top-right图片固定模式下右上角的点-
point-bottom-left图片固定模式下左下角的点-
point-bottom-right图片固定模式下右下角的点-

Preview Slots

暂无

Exposes

Viewport Exposes

名称说明类型
selectimport { selectImage } from 'vue-select-avatar'import('vue-select-avatar')['selectImage']
cropperimport { cropper } from 'vue-select-avatar'import('vue-select-avatar')['cropper']
initPosition初始化位置(res: { file: File, width: number, height: number }) => void

Preview Exposes

暂无

错误处理

vue-select-avatar 暴露了两个错误处理方法:isCancelErrorgetErrorMessage

ts
import { isCancelError, getErrorMessage } from 'vue-select-avatar'
  • isCancelError: 判断错误是否为取消错误
  • getErrorMessage: 获取错误信息(默认是中文错误信息)
ts
import { getErrorMessage, isCancelError, selectImage } from 'vue-select-avatar'

import { ElMessage } from 'element-plus'

selectImage().catch((err) => {
  // 取消
  if (isCancelError(err)) return
  // 错误处理
  console.error(err)
  ElMessage.error(getErrorMessage(err))
})

如果需要英文的错误信息,可以导入 import { errorMessageMap_EN } from 'vue-select-avatar/errorMessage_en.js' 并将 getErrorMessage 的第二个参数设置为 errorMessageMap_EN

ts
import { getErrorMessage, isCancelError, selectImage } from 'vue-select-avatar'
import { errorMessageMap_EN } from 'vue-select-avatar/errorMessage_en.js'

import { ElMessage } from 'element-plus'

selectImage({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
  // 取消
  if (isCancelError(err)) return
  // 错误处理
  console.error(err)
  ElMessage.error(getErrorMessage(err, errorMessageMap_EN))
})

也可以自定义错误信息

ts
import { getErrorMessage, isCancelError, selectImage, errorMessageMap } from 'vue-select-avatar'

import { ElMessage } from 'element-plus'

selectImage({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
  // 取消
  if (isCancelError(err)) return
  // 错误处理
  console.error(err)
  ElMessage.error(getErrorMessage(err, { ...errorMessageMap, CANCEL: '自定义错误信息' }))
})

完整的 errorMessageMap

ts
import type { ErrorMessageMap } from 'vue-select-avatar'

export const errorMessageMap: ErrorMessageMap = {
  UNKNOWN: '未知错误',
  CANCEL: '取消',
  NOT_IMAGE_FILE: '非图片文件',
  IMAGE_FILE_TOO_LARGE: '图片文件过大',
  IMAGE_TOO_SMALL: '图片尺寸过小',
  IMAGE_TOO_LARGE: '图片尺寸过大',
  IMAGE_LOAD_FAILED: '图片加载失败',
  CANVAS_TO_BLOB_FAILED: 'canvas 转 blob 失败',
  BLOB_TO_BASE64_FAILED: 'blob 转 base64 失败',
  CANVAS_CONTEXT_NOT_DEFINED: 'canvas context 未定义',
  NO_IMAGE_SELECTED: '未选择图片',
}