前端组件部分:
效果图如下:
使用之前需要 安装 VueCropper 和 image-conversion
npm i vue-cropper
npm i image-conversion
由于这两个包本文章都是局部引用,所以无需再全局配置
组件代码如下:可能更具自己的样式再去做对应的修改
<!--
* @描述: 证件照上传 图片部分(对接上传裁剪功能)
-->
<template>
<!-- :limit="limits" -->
<el-upload ref="uploadFiles" action="*" :before-upload="beforeAvatarUpload" :on-remove="handleRemove"
:on-success="handleSuccess" :on-change="handleChange" :multiple="multiple" :http-request="selectPicUpload"
:show-file-list="false" drag>
<div class="pic_uni" :style="{
'width': width,
'height': height,
'aspect-ratio': aspectRatio_out
}">
<img v-if="modelValue" class="" :src="modelValue" alt="" srcset="">
<div v-else class="def_box">
<slot>
<el-icon :size="34">
<Camera />
</el-icon>
<p style="font-size: 16px;">{{ describe }}</p>
</slot>
</div>
</div>
</el-upload>
<el-dialog destroy-on-close :title="title" v-model="dialogVisibleCorpper" width="800px" append-to-body>
<el-row>
<el-col :span="12" style="height: 300px">
<VueCropper ref="cropper" :img="options.img" :info="true" :autoCrop="options.autoCrop"
:autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox"
:fixed="options.fixed" :centerBox="options.centerBox" :fixedNumber="options.fixedNumber"
:outputType="options.outputType" @realTime="realTime" />
</el-col>
<el-col :span="12" style="height: 300px;display: flex;align-items: center;justify-content: center;">
<!-- :style="{ 'aspect-ratio': aspectRatio }" -->
<div class="preview-box" :style="previews.img">
<!-- -->
<img v-if="previews.url" style="width: 100%;" :src="previews.url" />
</div>
</el-col>
</el-row>
<el-row style="margin-top: 12px">
<el-col :span="12">
<el-row>
<el-col :span="8">
<el-upload action="#" :http-request="selectPicUpload" :before-upload="beforeUpload" :show-file-list="false">
<el-button>选择</el-button>
</el-upload>
</el-col>
<el-col :span="4">
<el-button :icon="Plus" @click="changeScale(1)"></el-button>
</el-col>
<el-col :span="4">
<el-button :icon="Minus" @click="changeScale(-1)"></el-button>
</el-col>
<el-col :span="4">
<el-button :icon="RefreshLeft" @click="rotateLeft()"></el-button>
</el-col>
<el-col :span="4">
<el-button :icon="RefreshRight" @click="rotateRight()"></el-button>
</el-col>
</el-row>
</el-col>
<el-col :span="4" :offset="8" style="margin-left: 22.3%">
<el-button type="primary" @click="determine(cropperBlob)">提 交</el-button>
</el-col>
</el-row>
</el-dialog>
</template>
<!-- 简单使用模版
<uploadPicUni v-model="businessPic" width="285px" describe="点击上传营业执照"></uploadPicUni>
-->
<script setup lang="ts">
import 'vue-cropper/dist/index.css'
import { VueCropper } from "vue-cropper";
import * as imageConversion from "image-conversion";
import { ref, reactive, getCurrentInstance, computed } from 'vue'
import type { UploadRequestOptions } from 'element-plus'
import {
Plus,
Minus,
RefreshLeft,
RefreshRight,
} from '@element-plus/icons-vue'
const { proxy } = getCurrentInstance() as any;
import { OssUpload as CryptoTools } from '@/utils/crypto'
const props = defineProps({
limits: {
type: Number,
default: 1,
},
multiple: {
type: Boolean,
default: false,
},
fixed: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '200px',
},
height: {
type: String,
default: 'auto',
},
modelValue: {
type: String,
},
/**width 和 height 都给的情况下不生效*/
fixedNumber: {
type: Array,
default: [5, 3],
},
aspectRatio_out: {
type: String,
default: '5 / 3',
},
title: {
type: String,
default: '上传图片',
},
describe: {
type: String,
default: '上传图片',
}
})
const aspectRatio = computed(() => {
return props.fixed ? `${props.fixedNumber[0]} / ${props.fixedNumber[1]}` : ''
})
const options = reactive({
img: null, // 裁剪图片的地址
autoCropWidth: 200, // 默认生成截图框宽度 默认容器的 80%
autoCropHeight: 200, // 默认生成截图框高度 默认容器的 80%
outputType: 'png', // 裁剪生成图片的格式 jpeg, png, webp
autoCrop: true, // 是否默认生成截图框
fixedBox: false, // 固定截图框大小
fixedNumber: props.fixedNumber,
fixed: props.fixed,
centerBox: true,
})
const previews = ref({
url: ''
})
/**裁剪后的图片的blob格式文件流 */
const cropperBlob = ref<Blob>()
const emit = defineEmits(['update:modelValue'])
/**节点 */
const uploadFiles = ref();
const cropper = ref();
const dialogVisibleCorpper = ref<boolean>(false);
const beforeAvatarUpload = () => { }
const handleRemove = () => { }
const handleSuccess = () => { }
const handleChange = () => { }
// 实时预览事件
const realTime = (data) => {
previews.value.img = {
width: data.w + 'px',
height: data.h + 'px',
}
cropper.value.getCropBlob(blob => {
cropperBlob.value = blob;
const url: string = URL.createObjectURL(blob);
previews.value.url = url;
})
}
const selectPicUpload = (obj: UploadRequestOptions) => {
let rawFile = obj.file;
if (rawFile.type !== "image/jpeg" && rawFile.type !== "image/png") {
proxy.$ShowMsg.error("只能上传jpg与png格式");
return false;
}
new Promise((resolve) => {
imageConversion.compress(rawFile, 0.4).then((res) => {
resolve(res);
});
}).then((res: Blob) => {
// 将 Blob 对象转换为 URL
const url = URL.createObjectURL(res);
options.img = url;
dialogVisibleCorpper.value = true;
})
}
// 修改图片大小 正数为变大 负数变小
const changeScale = (num) => {
num = num || 1
cropper.value.changeScale(num)
}
// 向左边旋转90度
const rotateLeft = () => {
cropper.value.rotateLeft()
}
// 向右边旋转90度
const rotateRight = () => {
cropper.value.rotateRight()
}
/**裁剪后的提交 */
const determine = (res: Blob) => {
CryptoTools.OSSUpload(res, ((data: any) => {
if (data.Success) {
dialogVisibleCorpper.value = false;
emit('update:modelValue', data.rows.url)
}
}))
}
</script>
<style scoped lang="less">
:deep(.el-upload-dragger) {
display: inline-block !important;
padding: 0;
}
:deep(.el-upload.is-drag) {
display: inline-block !important;
}
.el-upload {
.pic_uni {
aspect-ratio: 16/9;
img {
width: 100%;
height: 100%;
}
}
}
.avatar-container {
.img-box {
border: 1px solid #ccc;
width: 10vw;
height: 10vw;
}
}
.preview-box {
border: 1px solid #ccc;
overflow: hidden;
}
.def_box {
width: 100%;
height: 100%;
background-color: #f6f6f6;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #cfcfcf;
}
</style>
使用方式如下:
// businessPic就是双向绑定的图片 url
<uploadPicUni v-model="businessPic" width="285px" describe="点击上传营业执照"></uploadPicUni>
阿里云oss直传部分:
有部分小伙伴使用阿里云的oss直传可以参考下(使用了crypto进行解密后端返回的oss相关信息,依据自己的开发情况而定)
代码如下:
npm install dayjs
npm install ali-oss
npm install crypto-js
import CryptoJS from 'crypto-js';
import OSS from 'ali-oss';
import { Post } from '@/api/module/http';
import dayjs from 'dayjs';
// 加密
// // keyStr 设置的长度要不小于14位
export function encrypt(data: any, key: any = '123456789', iv: any = '123456789') {
if (typeof data === 'object') {
// 如果传入的data是json对象,先转义为json字符串
try {
data = JSON.stringify(data);
} catch (error) {
console.log('error:', error);
}
}
// 统一将传入的字符串转成UTF8编码
const dataHex = CryptoJS.enc.Utf8.parse(data); // 需要加密的数据
const keyHex = CryptoJS.enc.Utf8.parse(key); // 秘钥
const ivHex = CryptoJS.enc.Utf8.parse(iv); // 偏移量
const encrypted = CryptoJS.AES.encrypt(dataHex, keyHex, {
iv: ivHex,
mode: CryptoJS.mode.CBC, // 加密模式
padding: CryptoJS.pad.Pkcs7
});
let encryptedVal = encrypted.ciphertext.toString();
return encryptedVal; // 返回加密后的值
}
// 解密
// 解密数据
export function decrypt(data: any, key: any = '123456789', iv: any = '123456789') {
/*
传入的key和iv需要和加密时候传入的key一致
*/
// 统一将传入的字符串转成UTF8编码
let encryptedHexStr = CryptoJS.enc.Hex.parse(data);
// console.log(encryptedHexStr);
let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
const keyHex = CryptoJS.enc.Utf8.parse(key); // 秘钥
const ivHex = CryptoJS.enc.Utf8.parse(iv); // 偏移量
let decrypt = CryptoJS.AES.decrypt(srcs, keyHex, {
iv: ivHex,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
}
/**==========OSS上传========== */
export const OssUpload = {
/**解密 */
encryptionValue(encryptedText: any, key = 'admin', iv = 'admin') {
// 将密钥和初始向量解析为 CryptoJS 的 WordArray
const keyBytes = CryptoJS.enc.Utf8.parse(key);
const ivBytes = CryptoJS.enc.Utf8.parse(iv);
// 使用 DES 解密
const decrypted = CryptoJS.DES.decrypt(encryptedText, keyBytes, {
iv: ivBytes,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// 将解密后的 WordArray 转换为字符串
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
return decryptedText;
},
/**获取上传文件名称(本部分是后端生成的文件名 例如:936ad6a8-83b9-4bfe-92a9-b17a683cec97) 小伙伴可以自行生成,不能重复*/
GetFileName(callback: any) {
Post('/Oss/GetFileName', {}).then((res: any) => {
return callback(res.rows);
});
},
/**获取oss 配置参数(本文是在除login页面之后调用一次后端接口然后存放在了localStorage中,避免多次获取此信息) */
GetOssPara(callback: any) {
// console.log(parent.OSSCONFIG);
callback(JSON.parse(this.encryptionValue(localStorage.getItem('ossInfo'))));
},
/**调用oss官方 ali-oss库进行上传操作 */
uploadToOSS(file: any, oss: any, callback: any) {
var fileType = '';
if (file.name == undefined) {
switch (file.type) {
case 'image/jpeg':
fileType = 'jpg';
break;
case 'image/png':
fileType = 'png';
break;
default:
fileType = 'jpg';
}
} else {
const arr = file.name.split('.');
fileType = arr[arr.length - 1];
}
// 导入 ali-oss 库
const regionRegex = /\/\/([^\.]+)\.aliyuncs\.com/i;
const match = oss.Endpoint.match(regionRegex);
// 配置阿里云OSS参数
const client = new OSS({
region: match ? match[1] : oss.Endpoint,
accessKeyId: oss.AccessKeyId,
accessKeySecret: oss.AccessKeySecret,
bucket: oss.BucketName
});
try {
this.GetFileName(async (fileName: any) => {
const uniqueFileName = `${dayjs(new Date()).format(
'YYYY-MM-DD'
)}/${fileName}.${fileType}`;
// 上传文件到OSS
const result = await client.put(uniqueFileName, file);
const d = { Success: true, rows: result };
callback(d);
});
} catch (error) {
const d = { Success: false, rows: error };
callback(d);
}
},
/**oss直传整体实现
* @param {*} file 文件流数据
* @param {*} callback 回调函数
*/
OSSUpload(file: any, callback: any) {
this.GetOssPara((oss: any) => {
this.uploadToOSS(file, oss, (data: any) => {
callback(data);
});
});
}
};