适用于vue3+ts+ant-design-vue开发的项目
1.组件文件
<template>
<div class="d-inline d-flex">
<div class="d-inline upload-image d-flex" v-if="fileList">
<span
class="d-inline img-li d-relative mr20"
v-for="(item, index) in fileList"
>
<img
:src="IMGURL + item.filePath"
alt=""
@click="handlePreview(item.filePath)"
/>
<CloseCircleOutlined
v-if="!props.disabled"
@click="remove(item, index)"
class="closeImg"
/>
</span>
</div>
<a-upload
:action="uploadUrl"
list-type="picture-card"
@change="handleChange"
:before-upload="beforeUpload"
:disabled="props.disabled"
>
<div v-if="fileList.length < length">
<plus-outlined />
</div>
</a-upload>
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<img alt="example" style="width: 100%" :src="IMGURL + previewImage" />
</a-modal>
<a-modal
:visible="cropperVisible"
title="上传图片裁剪"
okText="确定"
cancelText="取消"
:width="
(options.autoCropWidth > 400 ? options.autoCropWidth : 400) + 80 + 'px'
"
@cancel="handleCancel1"
@ok="cropperSuccess"
>
<!-- 已上传图片 -->
<div
v-show="options.img"
class="avatar-crop"
:style="{
width: options.autoCropWidth + 'px',
height: options.autoCropHeight + 'px'
}"
>
<VueCropper
class="crop-box"
ref="cropper"
id="capture"
:img="options.img"
:autoCrop="options.autoCrop"
:fixedBox="options.fixedBox"
:canMoveBox="options.canMoveBox"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:centerBox="options.centerBox"
:fixed="options.fixed"
:canMove="options.canMove"
:canScale="options.canScale"
:outputType="options.outputType"
:original="true"
></VueCropper>
</div>
<div class="d-flex-jc-c mt25 wfull d-wrap">
<a-button class="mr20 mb5 mt5" type="primary" ghost @click="noCutting"
>不裁剪直接上传</a-button
>
<a-button class="mr20 mb5 mt5" type="primary" ghost @click="rotateLeft"
><RotateLeftOutlined />左旋转</a-button
>
<a-button class="mr20 mb5 mt5" type="primary" ghost @click="rotateRight"
><RotateRightOutlined />右旋转</a-button
>
<a-button
class="mr20 mb5 mt5"
type="primary"
ghost
@click="changeScale(1)"
><ZoomInOutlined />放大</a-button
>
<a-button class="mb5 mt5" type="primary" ghost @click="changeScale(-1)"
><ZoomOutOutlined />缩小</a-button
>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from "vue";
import { message } from "ant-design-vue";
import { publicMethods } from "@/utils/publicMethods"; // 数据处理方法
import { UploadApi, uploadScenicSpot, UploadApiNews } from "@/api/login"; // 接口
import { VueCropper } from "vue-cropper"; // 截图插件,需提前安装
const { getBase64, dataURLtoFile } = publicMethods();
const props = withDefaults(
defineProps<{ // 调用是要传的参数
fileList: any; // 图片数据
length: number; // 最大上传数量
type: number; // 图片类型
width: number; // 截图框的宽度
height: number; // 截图框的高度
cutting: boolean; // 是否截图 默认截图
disabled: boolean; // 是否禁止使用
urlType: number; // 图片上传地址类型,根据类型上传到不同的地址
}>(),
{ // 默认参数
width: 480,
height: 320,
cutting: true,
disabled: false,
urlType: 1
}
);
let uploadUrl = UploadApi;
if (props.urlType == 2) {
uploadUrl = UploadApiNews;
}
const IMGURL: any = process.env.UPLOAD_IMGURL;
// 裁剪
const options: any = reactive({
img: "", // 原图文件
outputSize: 0.2, // 裁剪生成图片的质量(可选 0.1-1)
autoCrop: true, // 默认生成截图框
fixedBox: true, // 固定截图框大小
canMoveBox: false, // 截图框可以拖动
autoCropWidth: "", // 截图框宽度
autoCropHeight: "", // 截图框高度
fixed: false, // 截图框宽高固定比例
fixedNumber: [4, 3], // 截图框的宽高比例
centerBox: false, // 截图框被限制在图片里面
canMove: true, // 上传图片不允许拖动
canScale: true, // 上传图片不允许滚轮缩放
outputType: "jpeg" // 裁剪生成图片的格式(jpeg || png || webp)
});
const cropper = ref<any>(null);
const infoFileList = ref<any>();
const previewVisible = ref<boolean>(false);
const cropperVisible = ref<boolean>(false);
const previewImage = ref<string | undefined>("");
const nweSrc = ref<any>("");
const fileData = ref<any>("");
// 大图展示取消按钮
const handleCancel = (type: number) => {
previewVisible.value = false;
};
// 裁剪取消按钮
const handleCancel1 = (type: number) => {
cropperVisible.value = false;
};
// 上传前操作
const beforeUpload = async (file: any) => {
fileData.value = file;
if (props.cutting) {
let img = await getBase64(file);
options.img = img;
cropperVisible.value = true;
}
return new Promise((resolve, reject) => {
if (props.cutting) {
return reject(true);
} else {
return resolve(true);
}
});
};
// 向左旋转
const rotateLeft = () => {
cropper.value.rotateLeft();
};
// 向右旋转
const rotateRight = () => {
cropper.value.rotateRight();
};
// 图片缩放
const changeScale = (num: any) => {
num = num || 1;
cropper.value.changeScale(num);
};
// 裁剪完成
const cropperSuccess = () => {
const el: any = document.querySelector("#capture");
cropper.value.getCropData((data: any) => {
nweSrc.value = data;
let file: any = dataURLtoFile(data);
requestUpload(file);
});
cropperVisible.value = false;
};
// 不裁剪
const noCutting = () => {
props.cutting = false;
requestUpload(fileData.value);
cropperVisible.value = false;
};
// 上传图片
const requestUpload = (file: any) => {
const param = new FormData();
param.append("file", file);
uploadScenicSpot(param, props.urlType).then((res: any) => {
if (res.code === 200) {
let info = {
file: "",
fileList: [
{
response: {
code: 200,
data: res.data
}
}
],
msg: "请求成功"
};
emit("uploadImgChange", {
type: props.type,
data: info.fileList
});
infoFileList.value = info;
}
});
};
// 选择要放大的图片
const handlePreview = (file: any) => {
previewImage.value = file;
previewVisible.value = true;
};
const emit = defineEmits(["uploadImgChange"]);
const handleChange = (info: any) => {
if (info.file.status === "uploading") {
return;
}
if (info.file.status === "done") {
emit("uploadImgChange", {
type: props.type,
data: info.fileList
});
infoFileList.value = info;
}
if (info.file.status === "error") {
message.error("upload error");
}
};
const remove = (item: any, index: number) => {
const ind = props.fileList.findIndex(
(file: any) => file.filePath === item.filePath
);
props.fileList.splice(ind, 1);
if (infoFileList.value) {
infoFileList.value.fileList.splice(index, 1);
}
};
onMounted(() => {
options.autoCropWidth = props.width;
options.autoCropHeight = props.height;
});
</script>
<style scoped lang="less">
/* you can make up upload button and sample style by using stylesheets */
.avatar-uploader > .ant-upload {
width: 128px;
height: 128px;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
/deep/ .ant-upload-list-picture-card {
display: none !important;
}
.upload-image {
.img-li {
img {
width: 104px;
height: 104px;
margin-right: 8px;
margin-bottom: 8px;
}
.closeImg {
position: absolute;
top: -5px;
right: 1px;
color: #ff3aa5;
background-color: #efefef;
border-radius: 50%;
font-size: 16px;
}
}
}
.avatar-crop {
margin: 0px auto;
}
</style>
2.接口文件
//上传图片 给ant-design-vue的上传组件提供的地址
export const UploadApi =
process.env.UPLOAD +
request.AxiosLink.UPLOAD_IMG_API +
"/sysUserUrl/upload/scenic";
//上传图片 给ant-design-vue的上传组件提供的地址 不同的图片上传到相对应的地址
export const UploadApiNews =
process.env.UPLOAD +
request.AxiosLink.UPLOAD_IMG_API +
"/sysUserUrl/upload/news";
//上传图片 手动上传时使用
export const uploadScenicSpot = (params: any, type: any) => {
let file = "scenicSpot";
if (type == 2) {
file = "news";
}
return axios
.post(
`${request.AxiosLink.UPLOAD_IMG_API}/sysUserUrl/upload/` + file,
params
)
.then((res) => res);
};
3.数据处理方法文件
import { ref } from "vue";
import DICLIST from "@/utils/dictionary-config.js"; // 状态字典文件,该组件中是用不到
export const publicMethods = () => {
const getBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
};
// base 64 转成二进制文件流
const dataURLtoFile = (urlData: any) => {
if (typeof urlData != "string") {
return;
}
var arr: any = urlData.split(",");
var type = arr[0].match(/:(.*?);/)[1];
var fileExt = type.split("/")[1];
var bstr = atob(arr[1]);
var n = bstr.length;
var u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], "filename." + fileExt, {
type: type
});
};
// const getBase64Image = (img: any) => {
// var canvas = document.createElement("canvas");
// canvas.width = img.width;
// canvas.height = img.height;
// let ctx: any = canvas.getContext("2d");
// ctx.drawImage(img, 0, 0, img.width, img.height);
// var ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
// var dataURL = canvas.toDataURL("image/" + ext);
// return dataURL;
// };
const getBase64Image = (Img: any) => {
let dataURL = "";
const canvas = document.createElement("canvas");
const { width } = Img;
const { height } = Img;
canvas.width = width;
canvas.height = height;
canvas.getContext("2d")!.drawImage(Img, 0, 0, width, height);
dataURL = canvas.toDataURL("image/jpeg"); //dataURL 图片base64 类型
return dataURL;
};
// 根据状态数值和类型返回状态文字描述
const dictionary = (status: number, type: string) => {
let statusName = "";
DICLIST.forEach((item: any) => {
if (item.key === type) {
const dic = item.data.find((value: any) => value.key === status);
statusName = dic ? dic.value : "";
} else {
statusName = "";
}
});
return statusName;
};
interface statusType {
key: number;
value: string;
color: string;
bgColor: string;
}
// 根据状态数值和类型返回状态所有值
const dictionaryData = (status: number, type: string) => {
let statusName = ref<statusType>();
DICLIST.forEach((item: any) => {
if (item.key === type) {
const dic = item.data.find((value: any) => value.key === status);
statusName = dic;
}
});
return statusName;
};
const MapLoader = () => {
return new Promise((resolve, reject) => {
if (window.AMap) {
resolve(window.AMap);
}
window.initAMap = () => {
resolve(window.AMap);
};
});
};
// 排序
const compare = (prop: any) => {
return (obj1: any, obj2: any) => {
var val1 = obj1[prop];
var val2 = obj2[prop];
if (val1 < val2) {
return -1;
} else if (val1 > val2) {
return 1;
} else {
return 0;
}
};
};
return {
getBase64,
dataURLtoFile,
getBase64Image,
dictionary,
dictionaryData,
MapLoader,
compare
};
};