Files
vue3/packages/upload-avatar/index.vue
2024-06-13 14:09:01 +08:00

252 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="upload-avatar">
<div class="el-upload el-upload--picture-card" @click="showDialog = !disabled">
<el-avatar v-if="avatarUrl" :src="avatarUrl" shape="square" :size="146" alt="头像" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</div>
<slot name="text">
<div v-show="!disabled" class="el-upload__tip w-400px">
<slot name="tip">
单张图片不大于{{ fileSize }}MB且格式为jpegjpgpng或bmp只可上传1张
</slot>
</div>
</slot>
</div>
<el-dialog v-if="!disabled" v-model="showDialog" :title="title" width="800px" append-to-body @opened="modalOpened" @closed="closeDialog">
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<VueCropper
v-if="visible" ref="cropperRef" :img="cropperURL" :info="true" :auto-crop="options.autoCrop"
:auto-crop-width="autoCropWidth" :auto-crop-height="autoCropHeight" :fixed-box="options.fixedBox"
:output-type="options.outputType" @real-time="realTime"
/>
</el-col>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<div
absolute h-200px w-200px rd="1/2" top="1/2" overflow-hidden
class="translate-x-1/2 -translate-y-1/2" :style="imageStyle"
>
<img v-show="options.previews.url" h-200px w-200px :src="options.previews.url" :style="options.previews.img" alt="实时预览图片">
</div>
</el-col>
</el-row>
<br>
<el-row>
<el-col :lg="2" :md="2">
<el-button @click="selectImage">
选择
<el-icon class="el-icon--right">
<Upload />
</el-icon>
</el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button :icon="Plus" @click="changeScale(1)" />
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button :icon="Minus" @click="changeScale(-1)" />
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button :icon="RefreshLeft" @click="rotateLeft()" />
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button :icon="RefreshRight" @click="rotateRight()" />
</el-col>
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
<el-button type="primary" :loading="loading" @click="confirmUpload">
</el-button>
</el-col>
</el-row>
</el-dialog>
</template>
<script lang="ts" setup name="ThUploadAvatar">
import { ElMessage } from 'element-plus'
import type { CSSProperties } from 'vue'
import { VueCropper } from 'vue-cropper'
import { Minus, Plus, RefreshLeft, RefreshRight, Upload } from '@element-plus/icons-vue'
const props = withDefaults(defineProps<{
accpet?: string
fileId?: number | string
fileUrl?: string
/**
* 图片大小单位MB
*
* @default 2
*/
fileSize?: number
/** 默认生成截图框宽度 */
autoCropHeight?: number
/** 默认生成截图框高度 */
autoCropWidth?: number
/** 上传请求函数 */
uploadFunction?: (formData: FormData) => Promise<FileVO>
/** 文件信息请求函数 */
fileFunction: (fileId?: number | string) => Promise<FileVO>
disabled?: boolean
title?: string
}>(), {
accpet: '.jpeg,.jpg,.png,.bmp',
fileId: '',
fileUrl: '',
fileSize: 2,
autoCropHeight: 200,
autoCropWidth: 200,
title: '上传头像',
})
const emit = defineEmits<{
(event: 'update:fileUrl', fileId?: number): void
(event: 'update:filelId', modelValue?: string): void
(event: 'onSuccess', file: FileVO): void
(event: 'onError', error: Error): void
}>()
let avatarUrl = $ref(props.fileUrl)
const filelId = useVModel(props, 'fileId', emit)
const imageStyle = {
boxShadow: '0 0 4px #ccc',
}
let loading = $ref(false)
interface PreviewData {
div: CSSProperties
h: number
html: string
img: CSSProperties
url: string
w: number
}
interface Option {
/** 裁剪图片的地址 */
img: any
/** 是否默认生成截图框 */
autoCrop: boolean
/** 固定截图框大小 不允许改变 */
fixedBox: boolean
/** 默认生成截图为PNG格式 */
outputType: string
/** 预览数据 */
previews: Partial<PreviewData>
visible: boolean
}
// 图片裁剪数据
const options = reactive<Option>({
img: '',
autoCrop: true,
fixedBox: true,
outputType: 'png',
previews: {},
visible: false,
})
const visible = ref(false)
const cropperRef = $ref<InstanceType<typeof VueCropper>>()
const { open, reset, onChange } = useFileDialog()
let showDialog = $ref(false)
/** 向左旋转 */
function rotateLeft() {
cropperRef?.rotateLeft()
}
/** 向右旋转 */
function rotateRight() {
cropperRef?.rotateRight()
}
/** 打开弹出层结束时的回调 */
function modalOpened() {
visible.value = true
}
/** 图片缩放 */
function changeScale(num?: number) {
cropperRef?.changeScale(num || 1)
}
/** 实时预览 */
function realTime(data: PreviewData) {
options.previews = data
}
const file = shallowRef()
const fileName = shallowRef()
const cropperURL = useObjectUrl(file)
onChange((param) => {
file.value = param?.item(0)
const flName = param?.item(0).name
fileName.value = flName.substring(0, flName.lastIndexOf('.'))
})
function selectImage() {
open({
accept: props.accpet,
multiple: false,
})
}
async function uploadImg(blob: Blob) {
if (!props.uploadFunction) {
ElMessage.error('请定义接口请求函数')
emit('onError', new Error('请定义接口请求函数'))
return
}
if (blob.size > props.fileSize * 1024 * 1024) {
ElMessage.error('您的图片经过剪裁依然很大,请重新选择图片')
emit('onError', new Error('您的图片经过剪裁依然很大,请重新选择图片'))
return
}
loading = true
const formData = new FormData()
formData.append('file', blob)
formData.append('name', fileName.value)
try {
const res = await props.uploadFunction(formData)
avatarUrl = res.url
filelId.value = res.id
showDialog = false
emit('onSuccess', res)
emit('update:fileUrl', res.url)
}
catch (err: any) {
emit('onError', err)
loading = false
}
}
function confirmUpload() {
cropperRef?.getCropBlob(uploadImg)
}
/** 关闭窗口 */
function closeDialog() {
options.visible = false
reset()
loading = false
}
async function init() {
if (!props.fileId) {
avatarUrl = props.fileUrl
return
}
const res = await props.fileFunction(props.fileId)
avatarUrl = res.url
}
tryOnMounted(init)
watch(() => props.fileId, init)
</script>
<style>
@import url('vue-cropper/dist/index.css');
@import url('element-plus/theme-chalk/el-upload.css');
</style>