Files
SaaS-lib/lib/component/form/upload/uploadPreCrop.vue
xuziqiang d0155dbe3c push
2024-05-15 17:29:42 +08:00

637 lines
20 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>
<div class="container clearfix">
<!-- 图片显示框 -->
<template v-if="currentImg.length">
<div class="imgList" v-for="(item, index) in fileList" :key="item">
<div class="imgContainer" @mouseenter="mouseEnter(index)" @mouseleave="mouseLeave(index)">
<img :src="currentImg[index]" class="imgCover" />
<span v-if="maskShow[index]" class="mask">
<EyeOutlined v-if="false" @click="handlePreview(item, index)" />
<DeleteOutlined v-if="!disabled" @click="deleteImg(index)" />
</span>
</div>
</div>
</template>
<!-- 图片显示框 /-->
<!-- 上传图片框/内置了input -->
<a-spin :spinning="spinning">
<a-upload
v-if="count === 1 ? 'true' : fileList.length < count"
list-type="picture-card"
:before-upload="beforeUpload"
@change="handleChange"
:multiple="count != 1"
:disabled="disabled"
:customRequest="selfUpload"
:showUploadList="false">
<!-- :disabled="count == 1 && fileUuid ? true : false" -->
<div ref="uploadRef">
<template v-if="isLt5M && isJpgOrPngOrJpeg">
<UploadOutlined :style="{ fontSize: '14px' }" />
<div class="ant-upload-text">上传图片</div>
</template>
<template v-else>
<LoadingOutlined />
<div class="ant-upload-text">校验失败</div>
</template>
</div>
</a-upload>
</a-spin>
<!-- 上传图片框/内置input /-->
</div>
<!-- 预览图片弹窗 -->
<ns-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<img v-if="previewVisible" alt="example" style="width: 100%" :src="previewImage" />
</ns-modal>
<!-- 预览图片弹窗 /-->
<!-- 错误消息提示 -->
<div class="err-msg" v-if="!isJpgOrPngOrJpeg">
<p>{{ fileName }} 文件上传失败</p>
<p :style="{ color: 'red' }">请选择{{ fileType.join(',') }}的图片</p>
</div>
<div class="err-msg" v-if="isJpgOrPngOrJpeg ? !isLt5M : ''">
<p>{{ fileName }} 文件上传失败</p>
<p :style="{ color: 'red' }">请选择{{ maxSize / 1024 / 1024 }}M内的图片</p>
</div>
<ns-modal
title="图片裁剪"
style="min-width: 980px"
:width="980"
:visible="cropPreview"
@cancel="cropperViewChange(false)">
<a-row :gutter="[40]">
<a-col :span="16" :style="{ height: '350px' }">
<vueCropper
ref="cropperRef"
:img="options.img"
:info="true"
:infoTrue="options.infoTrue"
:original="options.original"
:outputType="options.outputType"
:auto-crop="options.autoCrop"
:fixed-box="options.fixedBox"
:auto-crop-width="options.autoCropWidth"
:auto-crop-height="options.autoCropHeight"
:fixedNumber="options.fixedNumber"
:fixed="options.fixed"
:centerBox="options.centerBox"
@realTime="realTime" />
</a-col>
<a-col :span="8">
<div style="margin-bottom: 10px">预览区域</div>
<div style="margin: auto; width: 280px; overflow: auto">
<div v-html="previews.html"></div>
</div>
</a-col>
</a-row>
<template #footer>
<a-button @click="cropperViewChange(false)">取消</a-button>
<a-button type="primary" @click="rotateLeft">左旋转</a-button>
<a-button type="primary" @click="rotateRight">右旋转</a-button>
<a-button type="primary" @click="refreshCrop">复位</a-button>
<a-button type="primary" @click="uploadAgain">重新上传</a-button>
<a-button type="primary" @click="getCropSource">确定</a-button>
</template>
</ns-modal>
<!-- 错误消息提示 /-->
</div>
</template>
<script lang="ts">
import {
UploadOutlined,
LoadingOutlined,
EyeOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
import { defineComponent, ref, computed, reactive, watch } from 'vue';
import { http } from '/nerv-lib/util/http';
import { NsMessage } from '../../message';
import VueCropper from 'vue-cropper/src/vue-cropper.vue';
interface FileItem {
uid: string;
type: string;
size: number;
name?: string;
status?: string;
response?: string;
percent?: number;
url?: string;
preview?: string;
originFileObj?: any;
}
interface FileInfo {
file: FileItem;
fileList: FileItem[];
}
interface Options {
img: string | ArrayBuffer | null; // 裁剪图片的地址
info: true; // 裁剪框的大小信息
outputSize: number; // 裁剪生成图片的质量 [1至0.1]
outputType: 'jpeg'; // 裁剪生成图片的格式
canScale: boolean; // 图片是否允许滚轮缩放
autoCrop: boolean; // 是否默认生成截图框
autoCropWidth: number; // 默认生成截图框宽度
autoCropHeight: number; // 默认生成截图框高度
fixedBox: boolean; // 固定截图框大小 不允许改变
fixed: boolean; // 是否开启截图框宽高固定比例
fixedNumber: Array<number>; // 截图框的宽高比例 需要配合centerBox一起使用才能生效
full: boolean; // 是否输出原图比例的截图
canMoveBox: boolean; // 截图框能否拖动
original: boolean; // 上传图片按照原始比例渲染
centerBox: boolean; // 截图框是否被限制在图片里面
infoTrue: boolean; // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
}
// 转base64
function getBase64(file: File) {
return new Promise((resolve, reject) => {
// console.log(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
}
export default defineComponent({
name: 'NsUpload',
components: {
UploadOutlined,
LoadingOutlined,
DeleteOutlined,
EyeOutlined,
VueCropper,
},
props: {
// 上传的地址
url: {
type: String,
require: true,
},
// 上传的图片大小
maxSize: {
type: Number,
default: 5242880,
},
// 上传的图片类型
fileType: {
type: Array,
default: () => {
return ['jpg', 'png', 'jpeg'];
},
},
// 展示图片数量
count: {
type: Number,
default: 1,
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
// 上传文件类型0-证书1-图片2-身份证件
uploadType: {
type: Number,
default: 1,
},
baseImageUrl: {
type: [String, Object],
},
compatibilityUuid: {
type: [String, Object],
},
// 图片归类所需
params: {
type: [Object],
},
// 是否两个都emit
whetherTwo: {
type: Boolean,
default: false,
},
cropperProps: Object,
},
emits: ['change'],
setup(props, { emit }) {
console.log(props);
const previewVisible = ref<boolean>(false);
const cropPreview = ref<boolean>(false);
const isLt5M = ref<boolean>(true);
const isJpgOrPngOrJpeg = ref<boolean>(true);
const previewImage = ref<string | undefined>('');
const fileList = ref<FileItem[]>([]);
const fileUuid = ref<string>('');
const uuidList = ref([]);
const uuidString = ref('');
const currentImg = ref<string[]>([]);
const isIntImg = ref(false);
const cropperRef: Ref<any> = ref(null);
const previews: Ref<any> = ref({});
// 裁剪之后的数据
const realTime = (data: any) => {
previews.value = data;
};
const options: UnwrapNestedRefs<Options> = reactive({
img: '', // 需要剪裁的图片
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 280, // 默认生成截图框的宽度
autoCropHeight: 156, // 默认生成截图框的长度
fixedBox: false, // 是否固定截图框的大小 不允许改变
info: true, // 裁剪框的大小信息
outputSize: 0.5, // 裁剪生成图片的质量 [1至0.1]
outputType: 'jpeg', // 裁剪生成图片的格式
canScale: false, // 图片是否允许滚轮缩放
fixed: false, // 是否开启截图框宽高固定比例
fixedNumber: [16, 9], // 截图框的宽高比例 需要配合centerBox一起使用才能生效
full: true, // 是否输出原图比例的截图
canMoveBox: false, // 截图框能否拖动
original: false, // 上传图片按照原始比例渲染
centerBox: false, // 截图框是否被限制在图片里面
infoTrue: true, // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
...props.cropperProps,
});
const spinning = ref(false);
const uploadRef = ref();
const imgName = ref();
const filterValue = (value) => {
if (typeof value == 'object') {
let arr = [];
value.forEach((item) => {
if (item.includes('ParkPic/')) {
arr.push(item.slice(item.lastIndexOf('/') + 1));
} else {
arr.push(item);
}
});
return arr;
} else {
let str = '';
if (value) {
if (value.includes('ParkPic/')) {
str = value.slice(value.lastIndexOf('/') + 1);
} else {
str = value;
}
}
return str;
}
};
if (props.baseImageUrl) {
isIntImg.value = true;
if (typeof props.baseImageUrl == 'object') {
uuidList.value = props.compatibilityUuid;
// currentImg.value.concat(props.baseImageUrl);
props.baseImageUrl.map((item, index) => {
console.log(item);
fileList.value.push(item);
currentImg.value.push(item);
// previewImage.value = props.baseImageUrl[index];
});
// currentImg.value.unshift(...props.baseImageUrl);
emit(
'change',
props.whetherTwo
? [filterValue(fileList.value), filterValue(uuidList.value)]
: filterValue(fileList.value),
);
} else {
console.log(props);
fileList.value.push({});
uuidString.value = props.compatibilityUuid;
fileUuid.value = props.baseImageUrl;
currentImg.value.unshift(props.baseImageUrl);
previewImage.value = props.baseImageUrl;
emit(
'change',
props.whetherTwo
? [filterValue(fileUuid.value), filterValue(uuidString.value)]
: filterValue(fileUuid.value),
);
}
}
const fileName = ref<string | undefined>('');
const maskShow = ref<boolean[]>([]);
const acceptType = computed(() =>
props.fileType.map((item: String) => {
return 'image/' + item;
}),
);
watch(
() => props.baseImageUrl,
(e) => {
if (e) {
isIntImg.value = true;
if (typeof e == 'object') {
e.map((item, index) => {
fileList.value.push(item);
currentImg.value.push(item);
});
} else {
fileList.value.push({});
currentImg.value.unshift(e);
previewImage.value = e;
}
}
},
);
const beforeUpload = (file: FileItem) => {
// 上传出错后下次上传图片前重置为true让图片可以上传
isLt5M.value = true;
isJpgOrPngOrJpeg.value = true;
// 限制图片格式,服务器不支持gif图片
isJpgOrPngOrJpeg.value = acceptType.value.includes(file.type);
// 如果大于指定的大小,显示错误信息
if (file.size > props.maxSize) {
isLt5M.value = false;
}
fileName.value = file.name;
return isLt5M.value && isJpgOrPngOrJpeg.value;
};
const handleChange = ({ fileList: newFileList }: FileInfo) => {
// 单图上传
if (props.count === 1) {
// 删除图片时newFileList.length = 0
// 图片大小不符合规范时,!isLt5M.value为true
if (!isLt5M.value || !isJpgOrPngOrJpeg.value || newFileList.length - 1 < 0) {
// 让图片不显示,也不上传
fileList.value = [];
} else {
// 添加\更换图片
// newFileList[newFileList.length - 1]的目的是为了只显示最新一张图片
fileList.value = [newFileList[newFileList.length - 1]];
}
} else {
if (!isLt5M.value || !isJpgOrPngOrJpeg.value) {
} else {
fileList.value.push(newFileList);
}
}
};
const refreshCrop = () => {
cropperRef.value.refresh();
};
const rotateRight = () => {
cropperRef.value.rotateRight();
};
const rotateLeft = () => {
cropperRef.value.rotateLeft();
};
const cropperViewChange = (visible = false) => {
cropPreview.value = visible;
};
const uploadAgain = () => {
console.log(uploadRef.value);
uploadRef.value.click();
};
const getCropSource = () => {
cropperRef.value.getCropBlob(async (data: BlobPart) => {
let file = new window.File([data], imgName.value);
console.log(file);
if (props.count !== 1) {
currentImg.value.push(await getBase64(file));
} else {
currentImg.value = [await getBase64(file)];
}
const formData = new FormData();
formData.append('file', file);
formData.append('uploadType', props.uploadType);
Object.keys(props.params).map((item) => {
formData.append(item, props.params[item]);
});
request(formData).then(cropperViewChange(false));
});
};
const selfUpload = async ({ file }) => {
imgName.value = file.name;
options.img = await getBase64(file);
options.outputType = file.type.split('/')[1] == 'jpg' ? 'jpeg' : file.type.split('/')[1];
cropperViewChange(true);
};
const request = (formData) => {
const config = {
headers: {
'Content-Type': 'multipart/form-data',
},
// params: params,
};
spinning.value = true;
return http
.post(props.url, formData, config)
.then((res) => {
if (props.count > 1) {
fileList.value.push(res.data.httpUrl);
if (!uuidList.value) {
uuidList.value = [];
}
uuidList.value.push(res.data.fileUuid);
// fileList.value.push('/api/parking_merchant/objs/ParkPic/' + res.data.fileUuid);
fileList.value.forEach((item, index) => {
if (typeof item == 'object') {
fileList.value.splice(index, 1);
}
});
uuidList.value.forEach((item, index) => {
if (typeof item == 'object') {
uuidList.value.splice(index, 1);
}
});
// props.whetherTwo;
emit(
'change',
props.whetherTwo
? [filterValue(fileList.value), filterValue(uuidList.value)]
: filterValue(fileList.value),
);
} else {
fileUuid.value = res.data.httpUrl || res.data.picUuid || res.data.fileUuid;
uuidString.value = res.data.fileUuid;
emit(
'change',
props.whetherTwo
? [filterValue(fileUuid.value), filterValue(uuidString.value)]
: filterValue(fileUuid.value),
);
}
spinning.value = false;
})
.catch(() => {
fileList.value.forEach((item, index) => {
if (typeof item == 'object') {
fileList.value.splice(fileList.value.length - 1, 1);
}
});
NsMessage.error('上传失败,请重试');
spinning.value = false;
});
};
const handlePreview = async (item, index: number) => {
if (props.count > 1) {
previewImage.value = fileList.value[index];
} else {
previewImage.value = fileUuid.value;
}
previewVisible.value = true;
};
const deleteImg = (index: number) => {
currentImg.value.splice(index, 1);
fileList.value.splice(index, 1);
uuidList.value && uuidList.value.splice(index, 1);
uuidString.value = '';
fileUuid.value = '';
isIntImg.value = false;
if (props.count == 1) {
emit(
'change',
props.whetherTwo
? [filterValue(fileUuid.value), filterValue(uuidString.value)]
: filterValue(fileUuid.value),
);
} else {
emit(
'change',
props.whetherTwo
? [filterValue(fileList.value), filterValue(uuidList.value)]
: filterValue(fileList.value),
);
}
};
const handleCancel = () => {
previewVisible.value = false;
};
const mouseEnter = (index: number) => {
maskShow.value[index] = true;
};
const mouseLeave = (index: number) => {
maskShow.value[index] = false;
};
return {
previewVisible,
previewImage,
fileList,
isLt5M,
isJpgOrPngOrJpeg,
fileName,
currentImg,
maskShow,
fileUuid,
selfUpload,
handleCancel,
handlePreview,
handleChange,
beforeUpload,
mouseEnter,
mouseLeave,
deleteImg,
spinning,
options,
cropperRef,
realTime,
previews,
cropPreview,
rotateRight,
refreshCrop,
rotateLeft,
cropperViewChange,
getCropSource,
uploadRef,
uploadAgain,
};
},
});
</script>
<style lang="less" scoped>
:deep(.ant-upload-picture-card-wrapper) {
width: unset !important;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload.ant-upload-select-picture-card) {
margin: 0;
width: 64px !important;
height: 64px !important;
border: 1px solid #d9d9d9;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload.ant-upload-select-picture-card:hover) {
border-color: #d9d9d9;
}
:deep(.ant-upload-select-picture-card i) {
font-size: 32px;
color: #999;
}
:deep(.ant-upload-select-picture-card .ant-upload-text) {
color: #666;
font-size: 12px;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload-list-picture-card-container) {
width: 88px;
height: 64px !important;
}
:deep(.ant-upload-picture-card-wrapper .ant-upload-list-picture-card .ant-upload-list-item) {
margin: 0;
padding: 0;
width: 64px !important;
height: 64px !important;
}
.title,
.err-msg {
text-align: left;
}
.err-msg p {
margin: 0;
}
.container {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
.imgList {
display: flex;
}
.imgContainer {
margin-right: 16px;
border: 1px solid #d9d9d9;
position: relative;
margin-bottom: 8px;
}
.imgCover {
width: 64px;
height: 64px;
object-fit: contain;
}
.imgContainer .mask {
position: absolute;
top: 0;
left: 0;
width: 64px !important;
height: 64px !important;
background: rgba(0, 0, 0, 0.5);
text-align: center;
line-height: 64px !important;
}
.mask .anticon-eye {
color: white;
margin-right: 18px;
}
.mask .anticon-eye:hover {
color: #00acff;
margin-right: 18px;
}
.mask .anticon-delete {
color: white;
}
.mask .anticon-delete:hover {
color: #00acff;
}
</style>