<script lang="ts" setup>
|
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
|
|
import type { imgItem } from './props';
|
|
import { computed, ref, unref, watch } from 'vue';
|
|
import { useGlobSetting } from '@jnpf/hooks';
|
import { createImgPreview } from '@jnpf/ui';
|
|
import { $t } from '@vben/locales';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
|
import { DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
import { Upload as AUpload, UploadDragger as AUploadDragger, Form, message } from 'ant-design-vue';
|
|
import { units, uploadImgProps } from './props';
|
|
defineOptions({ inheritAttrs: false, name: 'JnpfUploadImg' });
|
const props = defineProps(uploadImgProps);
|
const emit = defineEmits(['update:value', 'change']);
|
const accessStore = useAccessStore();
|
const userStore = useUserStore();
|
const globSetting = useGlobSetting();
|
const fileList = ref<UploadFile[]>([]);
|
const imgList = ref<imgItem[]>([]);
|
const loading = ref<boolean>(false);
|
const apiUrl = ref(globSetting.apiURL);
|
const formItemContext = Form.useInjectFormItemContext();
|
|
const getAction = computed(() => `${globSetting.uploadURL}/${props.type}`);
|
const getHeaders: any = computed(() => ({ Authorization: accessStore.accessToken }));
|
const getUploadData = computed(() => ({
|
folder: props.folder,
|
pathType: props.pathType,
|
sortRule: (props.sortRule || []).join(','),
|
timeFormat: props.timeFormat,
|
}));
|
const getBindValues = computed(() => {
|
return {
|
accept: props.accept,
|
action: unref(getAction),
|
beforeUpload,
|
class: `img-uploader-${props.showType || 'card'}`,
|
data: unref(getUploadData),
|
disabled: props.disabled || props.detailed,
|
headers: unref(getHeaders),
|
maxCount: props.limit === 0 ? undefined : props.limit,
|
multiple: props.limit != 1,
|
onChange: handleChange,
|
showUploadList: false,
|
};
|
});
|
|
watch(
|
() => unref(props.value),
|
(val) => {
|
imgList.value = val && Array.isArray(val) ? val : [];
|
},
|
{ deep: true, immediate: true },
|
);
|
|
function beforeUpload(file) {
|
const isTopLimit = props.limit ? imgList.value.length >= props.limit : false;
|
if (isTopLimit) {
|
message.error($t('component.upload.imageMaxNumber', [props.limit]));
|
return AUpload.LIST_IGNORE;
|
}
|
const isAccept = /image\/*/.test(file.type);
|
if (!isAccept) {
|
message.error($t('component.upload.uploadImg'));
|
return AUpload.LIST_IGNORE;
|
}
|
if (!props.fileSize) return true;
|
const unitNum = units[props.sizeUnit];
|
const isRightSize = file.size / unitNum < props.fileSize;
|
if (!isRightSize) {
|
message.error($t('component.upload.imageMaxSize', { size: props.fileSize, unit: props.sizeUnit }));
|
return AUpload.LIST_IGNORE;
|
}
|
return true;
|
}
|
function handleChange({ file }: UploadChangeParam) {
|
if (file.status === 'uploading') return (loading.value = true);
|
if (file.status === 'error') {
|
loading.value = false;
|
fileList.value = fileList.value.filter((o) => o.uid != file.uid);
|
message.error($t('component.upload.uploadError'));
|
return;
|
}
|
if (file.status === 'done') {
|
loading.value = false;
|
if (file.response.code === 200) {
|
const isTopLimit = props.limit ? imgList.value.length >= props.limit : false;
|
if (isTopLimit) {
|
fileList.value = fileList.value.filter((o) => o.uid != file.uid);
|
message.error($t('component.upload.imageMaxNumber', [props.limit]));
|
return;
|
}
|
imgList.value.push({
|
fileId: file.response.data.name,
|
name: file.name,
|
thumbUrl: file.response.data.thumbUrl,
|
url: file.response.data.url,
|
});
|
emit('update:value', unref(imgList));
|
emit('change', unref(imgList));
|
formItemContext.onFieldChange();
|
} else {
|
fileList.value = fileList.value.filter((o) => o.uid != file.uid);
|
message.error(file.response.msg);
|
}
|
}
|
}
|
function getImgUrl(url) {
|
const userInfo: any = userStore.getUserInfo || {};
|
const securityKey = userInfo?.securityKey || '';
|
if (!securityKey) return apiUrl.value + url;
|
const realUrl = `${apiUrl.value + url + (url.includes('?') ? '&' : '?')}s=${securityKey}`;
|
return realUrl;
|
}
|
function handlePreview(index: number) {
|
const imageList = imgList.value.map((o) => getImgUrl(o.url));
|
createImgPreview({ imageList, index });
|
}
|
function handleRemove(index: number) {
|
fileList.value.splice(index, 1);
|
imgList.value.splice(index, 1);
|
emit('update:value', unref(imgList));
|
emit('change', unref(imgList));
|
formItemContext.onFieldChange();
|
}
|
</script>
|
|
<template>
|
<div :class="$attrs.class" class="upload-img-container">
|
<template v-if="!detailed">
|
<AUpload v-model:file-list="fileList" v-bind="getBindValues" v-if="showType === 'button'">
|
<a-button pre-icon="icon-ym icon-ym-btn-upload" :disabled="disabled">{{ buttonText }}</a-button>
|
</AUpload>
|
<div class="upload-img__tip mb-[10px]" v-if="tipText && showType === 'button'">{{ tipText }}</div>
|
<AUploadDragger v-model:file-list="fileList" v-bind="getBindValues" v-if="showType === 'dragger'">
|
<p class="ant-upload-drag-icon icon-ym icon-ym-btn-upload"></p>
|
<p class="ant-upload-text">{{ $t('component.upload.imgDragger') }}</p>
|
<div class="upload-img__tip" v-if="tipText">{{ tipText }}</div>
|
</AUploadDragger>
|
</template>
|
<transition-group v-if="!simple" class="ant-upload-list ant-upload-list-picture-card" name="list" tag="div">
|
<div v-for="(file, index) in imgList" :key="file.fileId" class="ant-upload-list-picture-card-container">
|
<div class="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
|
<div class="ant-upload-list-item-info">
|
<a class="ant-upload-list-item-thumbnail">
|
<img :src="getImgUrl(file.thumbUrl || file.url)" class="ant-upload-list-item-image" />
|
</a>
|
</div>
|
<span class="ant-upload-list-item-actions">
|
<EyeOutlined @click="handlePreview(index)" />
|
<DeleteOutlined v-if="!disabled && !detailed" @click="handleRemove(index)" />
|
</span>
|
</div>
|
</div>
|
</transition-group>
|
<p v-if="simple" class="link-text" @click="handlePreview(0)">{{ $t('component.upload.viewImage') }}</p>
|
<template v-if="!detailed && showType !== 'button' && showType !== 'dragger'">
|
<AUpload v-model:file-list="fileList" list-type="picture-card" v-bind="getBindValues">
|
<div>
|
<PlusOutlined />
|
<div class="ant-upload-text" v-if="buttonText">{{ buttonText }}</div>
|
</div>
|
</AUpload>
|
<div class="upload-img__tip" v-if="tipText">{{ tipText }}</div>
|
</template>
|
</div>
|
</template>
|
<style lang="scss" scoped>
|
.upload-img-container {
|
.ant-upload-list {
|
display: inline;
|
|
.ant-upload-list-picture-card-container {
|
display: inline-block;
|
width: 100px;
|
height: 100px;
|
margin: 0 8px 8px 0;
|
vertical-align: top;
|
|
.ant-upload-list-item {
|
position: relative;
|
height: 100%;
|
padding: 0;
|
overflow: hidden;
|
border-radius: var(--radius);
|
|
&:hover {
|
.ant-upload-list-item-info::before {
|
opacity: 1;
|
}
|
}
|
|
.ant-upload-list-item-info {
|
position: relative;
|
height: 100%;
|
overflow: hidden;
|
|
&::before {
|
position: absolute;
|
z-index: 1;
|
width: 100%;
|
height: 100%;
|
content: ' ';
|
background-color: rgb(0 0 0 / 45%);
|
opacity: 0;
|
transition: all 0.3s;
|
}
|
|
.ant-upload-list-item-thumbnail {
|
height: 100%;
|
|
.ant-upload-list-item-image {
|
width: 100%;
|
height: 100%;
|
object-fit: unset;
|
}
|
}
|
}
|
|
.ant-upload-list-item-actions {
|
position: absolute;
|
top: 50%;
|
left: 50%;
|
z-index: 10;
|
white-space: nowrap;
|
opacity: 0;
|
transform: translate(-50%, -50%);
|
transition: all 0.3s;
|
|
&:hover {
|
opacity: 1;
|
}
|
|
.anticon-eye,
|
.anticon-delete {
|
margin: 0 10px;
|
font-size: 20px;
|
color: rgb(255 255 255 / 85%);
|
}
|
}
|
}
|
}
|
}
|
|
.img-uploader-card {
|
width: 100px;
|
|
:deep(.ant-upload) {
|
width: 100px;
|
height: 100px;
|
|
.anticon {
|
font-size: 26px;
|
color: #8c939d;
|
}
|
|
.ant-upload-text {
|
margin-top: 10px;
|
color: #8c939d;
|
}
|
}
|
}
|
|
.img-uploader-button {
|
display: block;
|
margin-bottom: 10px;
|
}
|
|
.img-uploader-dragger {
|
display: block;
|
width: 300px;
|
max-width: 100%;
|
margin-bottom: 10px;
|
|
.ant-upload-drag-icon {
|
margin-bottom: 0;
|
font-size: 28px;
|
color: #8c939d;
|
}
|
|
.ant-upload-text {
|
font-size: 12px;
|
color: #8c939d;
|
}
|
}
|
|
.list-enter-active,
|
.list-leave-active {
|
transition: all 1s ease;
|
}
|
|
.list-enter-from,
|
.list-leave-to {
|
opacity: 0;
|
transform: translateY(-30px);
|
}
|
|
.upload-img__tip {
|
font-size: 12px;
|
line-height: 1.2;
|
color: var(--text-color-secondary);
|
word-break: break-all;
|
}
|
}
|
</style>
|
<style lang="scss">
|
.ant-table {
|
.upload-img-container {
|
.ant-upload-list {
|
white-space: pre-wrap;
|
}
|
}
|
}
|
</style>
|