快速开始
简介
vue-select-avatar 是一个基于 Vue3 的头像选择的库,它提供了一个头像选择器组件、一个预览组件和一些工具函数。相比上一个版本,仅提供一个函数,虽然更加便捷,但是自定义和扩展难度更高,现在这个版本仅提供核心组件/工具函数,它更加精简、低耦合。同时也在快速使用中提供使用案例,以供参考,可自行复制。
本库将头像裁剪流程拆分为三个独立环节,降低耦合度,提高灵活性:
- 选择图片:通过
selectImage函数从用户设备选择并验证图片 - 控制裁剪区域:使用
<Viewport/>组件交互式地调整裁剪区域 - 图片截取:调用
<Viewport/>暴露的crop方法,通过CropOptions配置输出最终图片
安装
npm i vue-select-avatarpnpm add vue-select-avataryarn add vue-select-avatarWARNING
3.0+ 版本仅支持 Vue 3.x,Vue 2.x 用户请使用 vue-select-avatar@2.x
使用
基础使用
<Viewport grid />查看完整代码
<script setup lang="ts">
import { Viewport, AvatarError, type ViewportInstance } from 'vue-select-avatar' // 引入组件/函数等
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const viewportRef = ref<ViewportInstance>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)
const handleSelect = () => {
viewportRef.value?.select({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
// 忽略取消错误
if (AvatarError.isCancel(err)) return
// 错误处理
console.error(err)
ElMessage.error(err instanceof Error ? err.message : String(err))
})
}
const handleCrop = async () => {
try {
const file = await viewportRef.value?.crop<File>({ format: '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(error instanceof Error ? error.message : String(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; margin: 0.5rem 0 0.2rem">
<button @click="handleSelect">选择图片</button>
<button @click="handleCrop">截取</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>配合预览组件
通过 ref 将 <Viewport/> 实例传递给 <Preview/> 组件,实现实时预览裁剪效果。
<script setup lang="ts">
import { Viewport, Preview, type ViewportInstance } from 'vue-select-avatar'
const viewportRef = ref<ViewportInstance>()
</script>
<template>
<Viewport ref="viewportRef" grid />
<Preview :viewportRef bg="#252526" />
</template>查看完整代码
<script setup lang="ts">
import { Viewport, Preview, AvatarError, type ViewportInstance } from 'vue-select-avatar' // 引入组件/函数等
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const viewportRef = ref<ViewportInstance>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)
const handleSelect = () => {
viewportRef.value?.select({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
// 忽略取消错误
if (AvatarError.isCancel(err)) return
// 错误处理
console.error(err)
ElMessage.error(err instanceof Error ? err.message : String(err))
})
}
const handleCrop = async () => {
try {
const file = await viewportRef.value?.crop<File>({ format: '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(error instanceof Error ? error.message : String(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; margin: 0.5rem 0 0.2rem">
<button @click="handleSelect">选择图片</button>
<button @click="handleCrop">截取</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>图片固定模式
设置 mode="fixed-image" 开启图片固定模式,此模式下鼠标/触摸控制的就是观察窗,而不是图片。
在这种模式下,用户可以通过拖动四角的控制点来调整裁剪区域的大小,适合需要精确控制裁剪范围的场景。
<Viewport mode="fixed-image" />查看完整代码
<script setup lang="ts">
import { Viewport, AvatarError, type ViewportInstance } from 'vue-select-avatar' // 引入组件/函数等
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const viewportRef = ref<ViewportInstance>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)
const handleSelect = () => {
viewportRef.value?.select({ maxFileSize: 20 * 1024 * 1024 }).catch((err) => {
// 忽略取消错误
if (AvatarError.isCancel(err)) return
// 错误处理
console.error(err)
ElMessage.error(err instanceof Error ? err.message : String(err))
})
}
const handleCrop = async () => {
try {
const file = await viewportRef.value?.crop<File>({ format: '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(error instanceof Error ? error.message : String(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; margin: 0.5rem 0 0.2rem">
<button @click="handleSelect">选择图片</button>
<button @click="handleCrop">截取</button>
</div>
<Viewport ref="viewportRef" grid mode="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>两种移动模式详解
<Viewport/> 组件提供两种移动模式,通过 mode 属性控制:
mode="fixed-view"(默认)
- 观察窗口固定,鼠标/触摸拖动的是图片
- 适合大多数场景,操作直观
- 滚轮缩放时,以鼠标中心为基准
mode="fixed-image"
- 图片固定,鼠标/触摸拖动的是观察窗口
- 可以通过四角控制点调整观察窗口大小
- 适合需要精确控制裁剪范围的场景
- 配合
minView和padding属性使用效果更佳
先选择图片再截取
使用 selectImage 选择完图片后,再通过 info prop 传入 Viewport 组件。
这种方式将图片选择和裁剪分离,可以在选择图片后进行额外的处理或验证,然后再显示裁剪界面。
<script setup lang="ts">
import {
Viewport,
AvatarError,
selectImage,
type ViewportInstance,
type ImageInfo,
} from 'vue-select-avatar'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const viewportRef = ref<ViewportInstance>()
const info = ref<ImageInfo>()
const handleSelect = async () => {
try {
const res = await selectImage({ maxFileSize: 20 * 1024 * 1024 })
info.value = res
} catch (error) {
// 忽略取消错误
if (AvatarError.isCancel(error)) return
// 错误处理
console.error(error)
ElMessage.error(error instanceof Error ? error.message : String(error))
}
}
</script>
<template>
<Viewport v-if="info" ref="viewportRef" grid :info="info" />
</template>查看完整代码
<script setup lang="ts">
import {
Viewport,
AvatarError,
selectImage,
type ImageInfo,
type ViewportInstance,
} from 'vue-select-avatar' // 引入组件/函数等
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const viewportRef = ref<ViewportInstance>()
const info = ref<ImageInfo>()
const src = ref('')
const fileSize = ref(0)
const size = ref(0)
const handleSelect = async () => {
try {
info.value = await selectImage({ maxFileSize: 20 * 1024 * 1024 })
} catch (error) {
// 忽略取消错误
if (AvatarError.isCancel(error)) return
// 错误处理
console.error(error)
ElMessage.error(error instanceof Error ? error.message : String(error))
}
}
const handleCrop = async () => {
try {
const file = await viewportRef.value?.crop<File>({ format: '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(error instanceof Error ? error.message : String(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; margin: 0.5rem 0 0.2rem">
<button @click="handleCrop">截取</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>INFO
本库提供两种选择图片的方式:
- 组件暴露的
select方法(通过<Viewport/>实例调用) - 独立提供的
selectImage方法
推荐使用 selectImage 方法,因为:
- 自定义程度更高,可以在选择前后进行任意处理
- 无需等待
<Viewport/>组件实例化,使用更灵活
自适应容器尺寸
<Viewport/> 组件的 size、width、height 属性支持设置为 'full',使组件填充满父容器。
<template>
<div style="width: 400px; height: 300px;">
<!-- 填充整个容器 (400x300) -->
<Viewport size="full" />
<!-- 宽度固定,高度填充整个容器 -->
<Viewport :width="320" height="full" />
</div>
</template><Preview/> 组件的 size 属性也支持设置为 'full'。当 size="full" 时,如果父容器的宽高不一致,将使用较小的那个值作为正方形的尺寸,确保预览图为正方形。
<template>
<div style="width: 400px; height: 300px;">
<!-- 会使用较小值 300px 作为正方形尺寸 -->
<Preview size="full" />
</div>
</template>键盘快捷键操作
<Viewport/> 组件支持丰富的键盘快捷键,提升操作效率:
移动操作
缩放操作
- 滚轮:正常速度缩放(步长由
step控制,默认 0.05) - Ctrl + 滚轮:精细缩放(步长由
ctrlStep控制,默认 0.02) - Shift + 滚轮:快速缩放(步长由
shiftStep控制,默认 2)
所有步长值 <1 表示比例,>=1 表示像素值。将步长设置为 0 可禁用对应的快捷键。
提示:当步长为数值时(>=1),建议设置为 2 的倍数(如 2、4、8、16),这样可以避免裁剪时出现小数像素,保证输出图片尺寸的准确性。
遮罩实现方式
<Viewport/> 提供两种遮罩实现方式,通过 mask 属性控制:
mask="clip"(默认):使用 CSSclip-path实现遮罩,性能更好mask="double":使用双层图片叠加实现遮罩,兼容性更好
自动降级机制:即使设置 mask="clip",如果浏览器不支持 clip-path,也会自动降级到双层图片模式,确保功能正常。
观察窗配置
通过 view 属性控制观察窗口的大小:
<template>
<!-- 观察窗为视口较小边的 60% -->
<Viewport :view="0.6" />
<!-- 观察窗固定为 200px -->
<Viewport :view="200" />
</template>说明:
- 当
view <= 1时,表示比例,基于<Viewport/>宽高中较小的那个值计算 - 当
view > 1时,表示像素值,直接使用指定的像素大小 - 默认值为
0.6(即视口较小边的 60%)
网格背景
启用 grid 属性可以显示透明网格背景,方便查看透明图片:
网格背景对于处理 PNG 等透明格式的图片特别有用,可以清晰看到透明区域。
滚动反向
通过 reverse 属性可以反转滚轮缩放方向:
<template>
<!-- 默认:向上滚动放大,向下滚动缩小 -->
<Viewport />
<!-- 反向:向上滚动缩小,向下滚动放大 -->
<Viewport reverse />
</template>提示:macOS 系统的触控板/鼠标滚轮方向与 Windows 相反,如果主要面向 mac 用户,建议开启 reverse 以获得更自然的操作体验。
自定义控制点样式
在 mode="fixed-image" 模式下,可以通过插槽自定义四个角的控制点样式:
<template>
<Viewport mode="fixed-image">
<template #point-top-left>
<div class="custom-point">↖</div>
</template>
<template #point-top-right>
<div class="custom-point">↗</div>
</template>
<template #point-bottom-left>
<div class="custom-point">↙</div>
</template>
<template #point-bottom-right>
<div class="custom-point">↘</div>
</template>
</Viewport>
</template>
<style scoped>
.custom-point {
width: 20px;
height: 20px;
background: #409eff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
</style>可用的插槽:
point-top-left:左上角控制点point-top-right:右上角控制点point-bottom-left:左下角控制点point-bottom-right:右下角控制点
Preview 组件的圆形模式
<Preview/> 组件支持圆形预览,通过 round 属性启用:
<template>
<!-- 方形预览(默认) -->
<Preview :viewport-ref="viewportRef" />
<!-- 圆形预览 -->
<Preview :viewport-ref="viewportRef" round />
<!-- 圆形预览 + 自定义背景色 -->
<Preview :viewport-ref="viewportRef" round bg="#f0f0f0" />
</template>还可以通过 bg 属性设置预览区域的背景颜色,默认为透明。
CSS 变量自定义
<Viewport/> 组件支持通过 CSS 变量自定义样式:
<template>
<Viewport
:border="2"
style="
--line-color: #409eff;
--point-size: 12px;
--mask-color: rgba(0, 0, 0, 0.6);
--bg-color: #1a1a1a;
"
/>
</template>可用的 CSS 变量:
--line-color:线条和控制点边框颜色(默认#fff)--point-size:控制点大小(默认10px)--mask-color:遮罩层颜色(默认rgba(0, 0, 0, 0.5))--bg-color:背景颜色(默认#000)--border-width:边框宽度(默认1px)
注意事项:
- 边框宽度:建议通过
borderprop 控制边框宽度,而不是直接设置--border-width,这样可以确保边框逻辑正确应用 - 控制点大小:
--point-size建议与padding属性保持一致,避免控制点与视口边缘重叠或间距过大
<template>
<!-- 推荐做法 -->
<Viewport :border="2" :padding="12" style="--point-size: 12px;" />
</template>错误处理
vue-select-avatar 使用 AvatarError 类来处理错误情况。
import { AvatarError } from 'vue-select-avatar'AvatarError.is(value): 判断错误是否为 AvatarError 实例AvatarError.isCancel(value): 判断错误是否为取消错误
import { AvatarError, selectImage } from 'vue-select-avatar'
import { ElMessage } from 'element-plus'
selectImage().catch((err) => {
// 取消
if (AvatarError.isCancel(err)) return
// 错误处理
console.error(err)
ElMessage.error(AvatarError.is(err) ? err.message : String(err))
})多语言支持
库内置了中文和英文两种语言支持,默认使用中文。可以通过 setLocale 函数切换语言:
import { setLocale, en } from 'vue-select-avatar'
// 切换到英文
setLocale(en)自定义错误信息
如果需要自定义错误信息,可以创建自己的 locale 对象:
import { setLocale } from 'vue-select-avatar'
import type { Locale } from 'vue-select-avatar'
const customLocale: Locale = {
UNKNOWN: '未知错误',
CANCEL: '用户取消操作',
NOT_IMAGE_FILES: '所选文件不是图片',
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上下文未定义',
NO_IMAGE_SELECTED: '未选择图片',
}
setLocale(customLocale)完整的错误码列表:
export type ErrorCode =
| 'UNKNOWN' // 未知错误
| 'CANCEL' // 取消
| 'NOT_IMAGE_FILES' // 非图片文件
| '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' // 未选择图片注意事项
<Preview />组件的根元素请勿设置box-sizing: border-box;,否则会导致位置偏差