<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>