You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
636 lines
20 KiB
636 lines
20 KiB
<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>
|
|
|