parent
250c14b3da
commit
149e71e481
16 changed files with 152 additions and 769 deletions
@ -1,118 +0,0 @@ |
|||||||
<template> |
|
||||||
<el-upload |
|
||||||
:action="action" |
|
||||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
|
||||||
:accept="accept" |
|
||||||
:before-upload="beforeUpload" |
|
||||||
:on-progress="(event, file) => (progressFile = file)" |
|
||||||
:show-file-list="false" |
|
||||||
:disabled="disabled" |
|
||||||
:multiple="multiple" |
|
||||||
> |
|
||||||
<!-- |
|
||||||
// 用于测试上传进度条 |
|
||||||
action="https://jsonplaceholder.typicode.com/posts/" |
|
||||||
--> |
|
||||||
<el-button type="primary">{{ $t('clickToUpload') }}</el-button> |
|
||||||
</el-upload> |
|
||||||
<el-progress v-if="progressFile.status === 'uploading'" :percentage="parseInt(progressFile.percentage, 10)"></el-progress> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script lang="ts"> |
|
||||||
import { defineComponent, onMounted, ref, toRefs, computed } from 'vue'; |
|
||||||
import { ElMessage } from 'element-plus'; |
|
||||||
import { useI18n } from 'vue-i18n'; |
|
||||||
import { getAuthHeaders } from '@/utils/auth'; |
|
||||||
import { getSiteHeaders } from '@/utils/common'; |
|
||||||
import { imageUploadUrl, videoUploadUrl, docUploadUrl, fileUploadUrl, queryGlobalSettings } from '@/api/config'; |
|
||||||
|
|
||||||
export default defineComponent({ |
|
||||||
name: 'BaseUpload', |
|
||||||
props: { |
|
||||||
type: { |
|
||||||
type: String, |
|
||||||
default: 'file', |
|
||||||
validator: (value: string) => ['image', 'video', 'doc', 'file'].includes(value), |
|
||||||
}, |
|
||||||
uploadAction: { type: String }, |
|
||||||
fileAccept: { type: String }, |
|
||||||
fileMaxSize: { type: Number }, |
|
||||||
multiple: { type: Boolean }, |
|
||||||
disabled: { type: Boolean, default: false }, |
|
||||||
'on-success': { type: Function }, |
|
||||||
}, |
|
||||||
setup(props) { |
|
||||||
const { type, uploadAction, fileAccept, fileMaxSize } = toRefs(props); |
|
||||||
const { t } = useI18n(); |
|
||||||
const progressFile = ref<any>({}); |
|
||||||
const global = ref<any>(); |
|
||||||
const fetchGlobalSettings = async () => { |
|
||||||
global.value = await queryGlobalSettings(); |
|
||||||
}; |
|
||||||
onMounted(() => { |
|
||||||
fetchGlobalSettings(); |
|
||||||
}); |
|
||||||
const action = computed(() => { |
|
||||||
if (uploadAction?.value != null) { |
|
||||||
return uploadAction.value; |
|
||||||
} |
|
||||||
switch (type.value) { |
|
||||||
case 'image': |
|
||||||
return imageUploadUrl; |
|
||||||
case 'video': |
|
||||||
return videoUploadUrl; |
|
||||||
case 'doc': |
|
||||||
return docUploadUrl; |
|
||||||
case 'file': |
|
||||||
return fileUploadUrl; |
|
||||||
default: |
|
||||||
throw new Error(`Type not support: ${type.value}`); |
|
||||||
} |
|
||||||
}); |
|
||||||
const accept = computed(() => { |
|
||||||
if (fileAccept?.value != null) { |
|
||||||
return fileAccept.value; |
|
||||||
} |
|
||||||
switch (type.value) { |
|
||||||
case 'image': |
|
||||||
return global?.value?.upload?.imageInputAccept ?? '.jpg,.jpeg,.png,.gif'; |
|
||||||
case 'video': |
|
||||||
return global?.value?.upload?.videoInputAccept ?? '.mp4,.m3u8'; |
|
||||||
case 'doc': |
|
||||||
return global?.value?.upload?.docInputAccept ?? '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx'; |
|
||||||
case 'file': |
|
||||||
return global?.value?.upload?.fileInputAccept ?? '.zip,.7z,.gz,.bz2,.iso,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.mp4,.m3u8,.mp3,.ogg'; |
|
||||||
default: |
|
||||||
throw new Error(`Type not support: ${type.value}`); |
|
||||||
} |
|
||||||
}); |
|
||||||
const maxSize = computed(() => { |
|
||||||
if (fileMaxSize?.value != null) { |
|
||||||
return fileMaxSize.value; |
|
||||||
} |
|
||||||
switch (type.value) { |
|
||||||
case 'image': |
|
||||||
return global?.value?.upload?.imageLimitByte ?? 0; |
|
||||||
case 'video': |
|
||||||
return global?.value?.upload?.videoLimitByte ?? 0; |
|
||||||
case 'doc': |
|
||||||
return global?.value?.upload?.docLimitByte ?? 0; |
|
||||||
case 'file': |
|
||||||
return global?.value?.upload?.fileLimitByte ?? 0; |
|
||||||
default: |
|
||||||
throw new Error(`Type not support: ${type.value}`); |
|
||||||
} |
|
||||||
}); |
|
||||||
const beforeUpload = (file: any) => { |
|
||||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
|
||||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
|
||||||
return false; |
|
||||||
} |
|
||||||
return true; |
|
||||||
}; |
|
||||||
return { progressFile, getAuthHeaders, getSiteHeaders, action, accept, beforeUpload }; |
|
||||||
}, |
|
||||||
}); |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" scoped></style> |
|
@ -1,136 +0,0 @@ |
|||||||
<template> |
|
||||||
<div class="w-full"> |
|
||||||
<el-upload :action="fileUploadUrl" |
|
||||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
|
||||||
:accept="accept" |
|
||||||
:before-upload="beforeUpload" |
|
||||||
:on-success="(res) => fileList.push({ name: res.name, url: res.url, length: res.size })" |
|
||||||
:on-progress="(event, file) => (progressFile = file)" |
|
||||||
:show-file-list="false" |
|
||||||
multiple> |
|
||||||
<!-- |
|
||||||
action="https://jsonplaceholder.typicode.com/posts/" |
|
||||||
--> |
|
||||||
<el-button type="primary">{{ $t('clickToUpload') }}</el-button> |
|
||||||
</el-upload> |
|
||||||
<el-progress v-if="progressFile.status === 'uploading'" |
|
||||||
:percentage="parseInt(progressFile.percentage, 10)"></el-progress> |
|
||||||
<transition-group tag="ul" |
|
||||||
:class="['el-upload-list', 'el-upload-list--text', { 'is-disabled': disabled }]" |
|
||||||
name="el-list"> |
|
||||||
<li v-for="file in fileList" |
|
||||||
:key="file.url" |
|
||||||
class="el-upload-list__item is-success"> |
|
||||||
<a class="el-upload-list__item-name" |
|
||||||
@click="handlePreview(file)"> |
|
||||||
<el-icon class="el-icon--document"> |
|
||||||
<Document /> |
|
||||||
</el-icon>{{ file.name }} |
|
||||||
</a> |
|
||||||
<label class="el-upload-list__item-status-label"> |
|
||||||
<el-icon class="el-icon--upload-success el-icon--circle-check"> |
|
||||||
<CircleCheck /> |
|
||||||
</el-icon> |
|
||||||
</label> |
|
||||||
<el-icon v-if="!disabled" |
|
||||||
class="el-icon--close" |
|
||||||
@click="fileList.splice(fileList.indexOf(file), 1)"> |
|
||||||
<Close /> |
|
||||||
</el-icon> |
|
||||||
</li> |
|
||||||
</transition-group> |
|
||||||
<el-dialog :title="$t('article.fileList.attribute')" |
|
||||||
v-model="previewVisible" |
|
||||||
top="5vh" |
|
||||||
:width="768" |
|
||||||
append-to-body> |
|
||||||
<el-form ref="form" |
|
||||||
:model="previewFile" |
|
||||||
label-width="150px"> |
|
||||||
<el-form-item prop="name" |
|
||||||
:label="$t('name')" |
|
||||||
:rules="{ required: true, message: () => $t('v.required') }"> |
|
||||||
<el-input v-model="previewFile.name" |
|
||||||
maxlength="100"></el-input> |
|
||||||
</el-form-item> |
|
||||||
<el-form-item prop="length" |
|
||||||
:label="$t('size')" |
|
||||||
:rules="{ required: true, message: () => $t('v.required') }"> |
|
||||||
<el-input v-model="previewFile.length" |
|
||||||
maxlength="19"> |
|
||||||
<template #append>Byte</template> |
|
||||||
</el-input> |
|
||||||
</el-form-item> |
|
||||||
<el-form-item prop="url" |
|
||||||
label="URL" |
|
||||||
:rules="{ required: true, message: () => $t('v.required') }"> |
|
||||||
<el-input v-model="previewFile.url" |
|
||||||
maxlength="255"></el-input> |
|
||||||
</el-form-item> |
|
||||||
<el-button @click.prevent="handleSubmit()" |
|
||||||
type="primary" |
|
||||||
native-type="submit">{{ $t('submit') }}</el-button> |
|
||||||
</el-form> |
|
||||||
</el-dialog> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script setup lang="ts"> |
|
||||||
import { onMounted, ref, toRefs, computed } from 'vue'; |
|
||||||
import { ElMessage } from 'element-plus'; |
|
||||||
import { Close, Document, CircleCheck } from '@element-plus/icons-vue'; |
|
||||||
import { useI18n } from 'vue-i18n'; |
|
||||||
import { getAuthHeaders } from '@/utils/auth'; |
|
||||||
import { getSiteHeaders } from '@/utils/common'; |
|
||||||
import { fileUploadUrl, queryGlobalSettings } from '@/api/config'; |
|
||||||
|
|
||||||
const props = defineProps({ |
|
||||||
modelValue: { type: Array, default: () => [] }, |
|
||||||
fileAccept: { type: String }, |
|
||||||
fileMaxSize: { type: Number }, |
|
||||||
disabled: { type: Boolean, default: false }, |
|
||||||
}); |
|
||||||
const emit = defineEmits({ 'update:modelValue': null }); |
|
||||||
|
|
||||||
const { fileAccept, fileMaxSize } = toRefs(props); |
|
||||||
const { t } = useI18n(); |
|
||||||
const { modelValue } = toRefs(props); |
|
||||||
const progressFile = ref<any>({}); |
|
||||||
const fileList = computed({ |
|
||||||
get: (): any[] => modelValue.value, |
|
||||||
set: (val) => emit('update:modelValue', val), |
|
||||||
}); |
|
||||||
const previewVisible = ref<boolean>(false); |
|
||||||
const previewFile = ref<any>({}); |
|
||||||
const form = ref<any>(); |
|
||||||
|
|
||||||
const handlePreview = (file: any) => { |
|
||||||
previewFile.value = file; |
|
||||||
previewVisible.value = true; |
|
||||||
}; |
|
||||||
const handleSubmit = () => { |
|
||||||
form.value.validate(async (valid: boolean) => { |
|
||||||
if (!valid) return; |
|
||||||
previewVisible.value = false; |
|
||||||
}); |
|
||||||
}; |
|
||||||
const global = ref<any>(); |
|
||||||
const fetchGlobalSettings = async () => { |
|
||||||
global.value = await queryGlobalSettings(); |
|
||||||
}; |
|
||||||
onMounted(() => { |
|
||||||
fetchGlobalSettings(); |
|
||||||
}); |
|
||||||
const defaultAccept = '.zip,.7z,.gz,.bz2,.iso,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.mp4,.m3u8,.mp3,.ogg'; |
|
||||||
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.fileInputAccept ?? defaultAccept); |
|
||||||
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.fileLimitByte ?? 0); |
|
||||||
const beforeUpload = (file: any) => { |
|
||||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
|
||||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
|
||||||
return false; |
|
||||||
} |
|
||||||
return true; |
|
||||||
}; |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" scoped></style> |
|
@ -1,84 +0,0 @@ |
|||||||
<template> |
|
||||||
<el-dialog :title="$t('imageCrop')" v-model="visible" @closed="destroyCropper()" top="5vh" :width="768" destroy-on-close append-to-body> |
|
||||||
<div class="text-center"> |
|
||||||
<img ref="imgRef" @load="initCropper()" :src="src" alt="" class="inline" style="max-height:410px" /> |
|
||||||
</div> |
|
||||||
<div class="text-right"> |
|
||||||
<el-button @click.prevent="handleSubmit()" type="primary" native-type="submit" class="mt-4">{{ $t('submit') }}</el-button> |
|
||||||
</div> |
|
||||||
</el-dialog> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script lang="ts"> |
|
||||||
import { computed, defineComponent, ref, toRefs } from 'vue'; |
|
||||||
import Cropper from 'cropperjs'; |
|
||||||
import 'cropperjs/dist/cropper.css'; |
|
||||||
import { cropImage } from '@/api/config'; |
|
||||||
|
|
||||||
export default defineComponent({ |
|
||||||
name: 'ImageCropper', |
|
||||||
props: { |
|
||||||
modelValue: { type: Boolean, required: true }, |
|
||||||
src: { type: String, default: null }, |
|
||||||
width: { type: Number }, |
|
||||||
height: { type: Number }, |
|
||||||
thumbnailWidth: { type: Number }, |
|
||||||
thumbnailHeight: { type: Number }, |
|
||||||
}, |
|
||||||
emits: { 'update:modelValue': null, success: null }, |
|
||||||
setup(props, { emit }) { |
|
||||||
const { modelValue, src, width, height, thumbnailWidth, thumbnailHeight } = toRefs(props); |
|
||||||
const visible = computed({ |
|
||||||
get: () => modelValue.value, |
|
||||||
set: (val) => emit('update:modelValue', val), |
|
||||||
}); |
|
||||||
|
|
||||||
const imgRef = ref<any>(); |
|
||||||
const cropper = ref<any>(); |
|
||||||
const cropParam = ref<any>({}); |
|
||||||
|
|
||||||
const initCropper = () => { |
|
||||||
if (imgRef.value) { |
|
||||||
cropper.value = new Cropper(imgRef.value, { |
|
||||||
aspectRatio: width?.value && height?.value ? width.value / height.value : NaN, |
|
||||||
autoCropArea: width?.value && height?.value ? 1 : 0.8, |
|
||||||
viewMode: 1, |
|
||||||
minCropBoxWidth: width?.value ?? 16, |
|
||||||
minCropBoxHeight: height?.value ?? 16, |
|
||||||
zoomable: false, |
|
||||||
crop(event) { |
|
||||||
cropParam.value.url = src.value; |
|
||||||
cropParam.value.x = Math.floor(event.detail.x); |
|
||||||
cropParam.value.y = Math.floor(event.detail.y); |
|
||||||
cropParam.value.width = Math.floor(event.detail.width); |
|
||||||
cropParam.value.height = Math.floor(event.detail.height); |
|
||||||
cropParam.value.maxWidth = width?.value; |
|
||||||
cropParam.value.maxHeight = height?.value; |
|
||||||
cropParam.value.thumbnailWidth = thumbnailWidth?.value; |
|
||||||
cropParam.value.thumbnailHeight = thumbnailHeight?.value; |
|
||||||
}, |
|
||||||
}); |
|
||||||
} |
|
||||||
}; |
|
||||||
const destroyCropper = () => { |
|
||||||
if (cropper.value) { |
|
||||||
cropper.value.destroy(); |
|
||||||
} |
|
||||||
}; |
|
||||||
const handleSubmit = async () => { |
|
||||||
visible.value = false; |
|
||||||
emit('success', (await cropImage(cropParam.value)).url); |
|
||||||
}; |
|
||||||
return { imgRef, visible, initCropper, destroyCropper, handleSubmit }; |
|
||||||
}, |
|
||||||
}); |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" scoped> |
|
||||||
/* Ensure the size of the image fit the container perfectly */ |
|
||||||
:deep(img) { |
|
||||||
display: block; |
|
||||||
/* This rule is very important, please don't ignore this */ |
|
||||||
max-width: 100%; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,157 +0,0 @@ |
|||||||
<template> |
|
||||||
<div> |
|
||||||
<!-- <transition-group tag="ul" :class="['el-upload-list', 'el-upload-list--picture-card', { 'is-disabled': disabled }]" name="el-list"> --> |
|
||||||
<ul :class="['el-upload-list', 'el-upload-list--picture-card', { 'is-disabled': disabled }]"> |
|
||||||
<li v-for="file in fileList" |
|
||||||
:key="file.url" |
|
||||||
class="el-upload-list__item is-success"> |
|
||||||
<div class="w-full h-full bg-gray-50 flex justify-center items-center"> |
|
||||||
<img class="max-w-full max-h-full block" |
|
||||||
:src="file.url" |
|
||||||
alt="" /> |
|
||||||
<div class="full-flex-center absolute rounded-md cursor-default bg-black bg-opacity-50 opacity-0 hover:opacity-100 space-x-4" |
|
||||||
@click.stop> |
|
||||||
<el-icon class="image-action" |
|
||||||
@click="(cropperVisible = true), (currentFile = file)" |
|
||||||
:title="$t('cropImage')"> |
|
||||||
<Crop /> |
|
||||||
</el-icon> |
|
||||||
<el-icon class="image-action" |
|
||||||
@click="handlePreview(file)" |
|
||||||
:title="$t('previewImage')"> |
|
||||||
<View /> |
|
||||||
</el-icon> |
|
||||||
<el-icon class="image-action" |
|
||||||
@click="fileList.splice(fileList.indexOf(file), 1)" |
|
||||||
:title="$t('deleteImage')"> |
|
||||||
<Delete /> |
|
||||||
</el-icon> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</li> |
|
||||||
</ul> |
|
||||||
<!-- </transition-group> --> |
|
||||||
<el-upload :action="imageUploadUrl" |
|
||||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
|
||||||
:data="getData()" |
|
||||||
:accept="accept" |
|
||||||
:before-upload="beforeUpload" |
|
||||||
:on-success="(res, file) => fileList.push({ name: res.name, url: res.url })" |
|
||||||
:on-progress="(event, file) => (progressFile = file)" |
|
||||||
:show-file-list="false" |
|
||||||
multiple |
|
||||||
class="inline-block"> |
|
||||||
<el-progress v-if="progressFile.status === 'uploading'" |
|
||||||
type="circle" |
|
||||||
:percentage="parseInt(progressFile.percentage, 10)" /> |
|
||||||
<div v-else |
|
||||||
class="el-upload--picture-card"> |
|
||||||
<el-icon> |
|
||||||
<Plus /> |
|
||||||
</el-icon> |
|
||||||
</div> |
|
||||||
</el-upload> |
|
||||||
<div> |
|
||||||
<el-dialog v-model="previewVisible" |
|
||||||
top="5vh" |
|
||||||
:width="768"> |
|
||||||
<el-input v-model="previewFile.url" |
|
||||||
maxlength="255"> |
|
||||||
<template #prepend>URL</template> |
|
||||||
</el-input> |
|
||||||
<el-input v-model="previewFile.description" |
|
||||||
type="textarea" |
|
||||||
:rows="2" |
|
||||||
:placeholder="$t('article.imageList.description')" |
|
||||||
class="mt-1"></el-input> |
|
||||||
<img :src="previewFile.url" |
|
||||||
alt="" |
|
||||||
class="mt-1 border border-gray-300" /> |
|
||||||
</el-dialog> |
|
||||||
</div> |
|
||||||
<image-cropper v-model="cropperVisible" |
|
||||||
:src="currentFile.url" |
|
||||||
:thumbnailWidth="thumbnailWidth" |
|
||||||
:thumbnailHeight="thumbnailHeight" |
|
||||||
@success="(url) => (currentFile.url = url)"></image-cropper> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script setup lang="ts"> |
|
||||||
import { onMounted, ref, toRefs, computed } from 'vue'; |
|
||||||
import { ElMessage } from 'element-plus'; |
|
||||||
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue'; |
|
||||||
import { useI18n } from 'vue-i18n'; |
|
||||||
import { getAuthHeaders } from '@/utils/auth'; |
|
||||||
import { getSiteHeaders } from '@/utils/common'; |
|
||||||
import { imageUploadUrl, queryGlobalSettings } from '@/api/config'; |
|
||||||
import ImageCropper from './ImageCropper.vue'; |
|
||||||
|
|
||||||
const props = defineProps({ |
|
||||||
modelValue: { type: Array, default: () => [] }, |
|
||||||
fileAccept: { type: String }, |
|
||||||
fileMaxSize: { type: Number }, |
|
||||||
maxWidth: { type: Number }, |
|
||||||
maxHeight: { type: Number }, |
|
||||||
disabled: { type: Boolean, default: false }, |
|
||||||
}); |
|
||||||
|
|
||||||
const emit = defineEmits({ 'update:modelValue': null }); |
|
||||||
|
|
||||||
const { modelValue, maxWidth, maxHeight, fileAccept, fileMaxSize } = toRefs(props); |
|
||||||
const { t } = useI18n(); |
|
||||||
const progressFile = ref<any>({}); |
|
||||||
const currentFile = ref<any>({}); |
|
||||||
const previewVisible = ref<boolean>(false); |
|
||||||
const cropperVisible = ref<boolean>(false); |
|
||||||
const previewFile = ref<any>({ src: 'data:;base64,=' }); |
|
||||||
const fileList = computed({ |
|
||||||
get: (): any => modelValue.value, |
|
||||||
set: (val) => emit('update:modelValue', val), |
|
||||||
}); |
|
||||||
const handlePreview = (file: any) => { |
|
||||||
previewFile.value = file; |
|
||||||
previewVisible.value = true; |
|
||||||
}; |
|
||||||
const thumbnailWidth = 300; |
|
||||||
const thumbnailHeight = 300; |
|
||||||
const getData = () => { |
|
||||||
const data: any = { isWatermark: true, thumbnailWidth, thumbnailHeight }; |
|
||||||
if (maxWidth?.value != null) { |
|
||||||
data.maxWidth = maxWidth.value; |
|
||||||
} |
|
||||||
if (maxHeight?.value != null) { |
|
||||||
data.maxHeight = maxHeight.value; |
|
||||||
} |
|
||||||
return data; |
|
||||||
}; |
|
||||||
const global = ref<any>(); |
|
||||||
const fetchGlobalSettings = async () => { |
|
||||||
global.value = await queryGlobalSettings(); |
|
||||||
}; |
|
||||||
onMounted(() => { |
|
||||||
fetchGlobalSettings(); |
|
||||||
}); |
|
||||||
const defaultAccept = 'image/jpg,image/jpeg,image/png,image/gif'; |
|
||||||
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.imageInputAccept ?? defaultAccept); |
|
||||||
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.imageLimitByte ?? 0); |
|
||||||
const beforeUpload = (file: any) => { |
|
||||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
|
||||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
|
||||||
return false; |
|
||||||
} |
|
||||||
return true; |
|
||||||
}; |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" scoped> |
|
||||||
:deep(.el-dialog__headerbtn) { |
|
||||||
top: 4px; |
|
||||||
} |
|
||||||
.full-flex-center { |
|
||||||
@apply w-full h-full flex justify-center items-center; |
|
||||||
} |
|
||||||
.image-action { |
|
||||||
@apply cursor-pointer text-xl text-white; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,149 +0,0 @@ |
|||||||
<template> |
|
||||||
<el-upload :action="imageUploadUrl" |
|
||||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
|
||||||
:accept="accept" |
|
||||||
:before-upload="beforeUpload" |
|
||||||
:data="data" |
|
||||||
:show-file-list="false" |
|
||||||
:on-success="(res) => ((src = res.url), (cropperVisible = mode === 'manual'))" |
|
||||||
:on-progress="(event, file) => (progressFile = file)"> |
|
||||||
<!-- |
|
||||||
// 用于测试上传进度条 |
|
||||||
action="https://jsonplaceholder.typicode.com/posts/" |
|
||||||
--> |
|
||||||
<div v-if="src" |
|
||||||
class="full-flex-center rounded-border relative hover:border-opacity-0"> |
|
||||||
<img :src="src" |
|
||||||
class="max-w-full max-h-full block" /> |
|
||||||
<div class="full-flex-center absolute rounded-md cursor-default bg-black bg-opacity-50 opacity-0 hover:opacity-100 space-x-4" |
|
||||||
@click.stop> |
|
||||||
<el-icon class="image-action" |
|
||||||
@click="cropperVisible = true" |
|
||||||
:title="$t('cropImage')"> |
|
||||||
<Crop /> |
|
||||||
</el-icon> |
|
||||||
<el-icon class="image-action" |
|
||||||
@click="previewVisible = true" |
|
||||||
:title="$t('previewImage')"> |
|
||||||
<View /> |
|
||||||
</el-icon> |
|
||||||
<el-icon class="image-action" |
|
||||||
@click="src = undefined" |
|
||||||
:title="$t('deleteImage')"> |
|
||||||
<Delete /> |
|
||||||
</el-icon> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<el-progress v-else-if="progressFile.status === 'uploading'" |
|
||||||
type="circle" |
|
||||||
:percentage="parseInt(progressFile.percentage, 10)" /> |
|
||||||
<div v-else |
|
||||||
class="el-upload--picture-card"> |
|
||||||
<el-icon> |
|
||||||
<plus /> |
|
||||||
</el-icon> |
|
||||||
</div> |
|
||||||
</el-upload> |
|
||||||
<div> |
|
||||||
<el-dialog v-model="previewVisible" |
|
||||||
top="5vh" |
|
||||||
:width="768" |
|
||||||
append-to-body |
|
||||||
destroy-on-close> |
|
||||||
<el-input v-model="src"> |
|
||||||
<template #prepend>URL</template> |
|
||||||
</el-input> |
|
||||||
<img :src="src" |
|
||||||
alt="" |
|
||||||
class="mt-1 border border-gray-300" /> |
|
||||||
</el-dialog> |
|
||||||
</div> |
|
||||||
<image-cropper v-model="cropperVisible" |
|
||||||
:src="src" |
|
||||||
:width="width" |
|
||||||
:height="height" |
|
||||||
@success="(url) => (src = url)"></image-cropper> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script setup lang="ts"> |
|
||||||
import { computed, onMounted, ref, toRefs } from 'vue'; |
|
||||||
import { ElMessage } from 'element-plus'; |
|
||||||
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue'; |
|
||||||
import { useI18n } from 'vue-i18n'; |
|
||||||
import { getAuthHeaders } from '@/utils/auth'; |
|
||||||
import { getSiteHeaders } from '@/utils/common'; |
|
||||||
import { imageUploadUrl, queryGlobalSettings } from '@/api/config'; |
|
||||||
import ImageCropper from './ImageCropper.vue'; |
|
||||||
|
|
||||||
// 'image/jpg,image/jpeg,image/png,image/gif' |
|
||||||
|
|
||||||
const props = defineProps({ |
|
||||||
modelValue: { type: String, default: null }, |
|
||||||
fileAccept: { type: String }, |
|
||||||
fileMaxSize: { type: Number }, |
|
||||||
width: { type: Number }, |
|
||||||
height: { type: Number }, |
|
||||||
mode: { type: String, default: 'none' }, |
|
||||||
}); |
|
||||||
|
|
||||||
const emit = defineEmits({ 'update:modelValue': null }); |
|
||||||
|
|
||||||
const { modelValue, width, height, mode, fileAccept, fileMaxSize } = toRefs(props); |
|
||||||
const { t } = useI18n(); |
|
||||||
const progressFile = ref<any>({}); |
|
||||||
const previewVisible = ref<boolean>(false); |
|
||||||
const cropperVisible = ref<boolean>(false); |
|
||||||
const src = computed({ |
|
||||||
get: (): string | undefined => modelValue.value, |
|
||||||
set: (val: string | undefined) => emit('update:modelValue', val), |
|
||||||
}); |
|
||||||
const resizable = computed(() => ['cut', 'resize'].includes(mode.value)); |
|
||||||
const data = computed(() => { |
|
||||||
const params: any = { resizeMode: mode.value === 'cut' ? 'cut' : 'normal' }; |
|
||||||
if (width?.value != null) { |
|
||||||
// 为0不限制,为空则依然受全局图片宽高限制 |
|
||||||
params.maxWidth = resizable.value ? width.value : 0; |
|
||||||
} |
|
||||||
if (height?.value != null) { |
|
||||||
// 为0不限制,为空则依然受全局图片宽高限制 |
|
||||||
params.maxHeight = resizable.value ? height.value : 0; |
|
||||||
} |
|
||||||
return params; |
|
||||||
}); |
|
||||||
const global = ref<any>(); |
|
||||||
const fetchGlobalSettings = async () => { |
|
||||||
global.value = await queryGlobalSettings(); |
|
||||||
}; |
|
||||||
onMounted(() => { |
|
||||||
fetchGlobalSettings(); |
|
||||||
}); |
|
||||||
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.imageInputAccept ?? 'image/jpg,image/jpeg,image/png,image/gif'); |
|
||||||
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.imageLimitByte ?? 0); |
|
||||||
const beforeUpload = (file: any) => { |
|
||||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
|
||||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
|
||||||
return false; |
|
||||||
} |
|
||||||
return true; |
|
||||||
}; |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" scoped> |
|
||||||
:deep(.el-dialog__headerbtn) { |
|
||||||
top: 4px; |
|
||||||
} |
|
||||||
:deep(.el-upload) { |
|
||||||
width: 148px; |
|
||||||
height: 148px; |
|
||||||
} |
|
||||||
.full-flex-center { |
|
||||||
@apply w-full h-full flex justify-center items-center; |
|
||||||
} |
|
||||||
.rounded-border { |
|
||||||
border: 1px solid #c0ccda; |
|
||||||
@apply rounded-md bg-gray-50; |
|
||||||
} |
|
||||||
.image-action { |
|
||||||
@apply cursor-pointer text-xl text-white; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,4 +0,0 @@ |
|||||||
export { default as ImageUpload } from './ImageUpload.vue'; |
|
||||||
export { default as ImageListUpload } from './ImageListUpload.vue'; |
|
||||||
export { default as FileListUpload } from './FileListUpload.vue'; |
|
||||||
export { default as BaseUpload } from './BaseUpload.vue'; |
|
@ -1,14 +1,10 @@ |
|||||||
import defaultSettings from '@/settings'; |
import defaultSettings from '@/settings'; |
||||||
import i18n from '@/i18n'; |
|
||||||
|
|
||||||
const { title } = defaultSettings; |
const { title } = defaultSettings; |
||||||
|
|
||||||
export default function getPageTitle(pageTitle: string | undefined): string { |
export default function getPageTitle(pageTitle: string | undefined): string { |
||||||
if (pageTitle) { |
if (pageTitle) { |
||||||
const { |
return `${pageTitle} - ${title}`; |
||||||
global: { t }, |
|
||||||
} = i18n; |
|
||||||
return `${t(pageTitle)} - ${title}`; |
|
||||||
} |
} |
||||||
return `${title}`; |
return `${title}`; |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue