252 lines
6.6 KiB
Vue
252 lines
6.6 KiB
Vue
<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,且格式为jpeg、jpg、png或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">
|
||
提 交 yyds
|
||
</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>
|