first commit
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="file-uploader">
|
||||
<label class="file-uploader__dropzone-wrap">
|
||||
<div
|
||||
class="file-uploader__dropzone"
|
||||
:class="{ 'file-uploader__dropzone--active': isDragging }"
|
||||
@dragover.prevent
|
||||
@dragenter="isDragging = true"
|
||||
@dragleave="isDragging = false"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<SvgIcon name="upload" :size="32" class="file-uploader__upload-icon" />
|
||||
<span class="file-uploader__upload-text">{{ uploadText }}</span>
|
||||
<input
|
||||
type="file"
|
||||
class="file-uploader__input"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div v-if="modelValue.length > 0" class="file-uploader__list">
|
||||
<div v-for="(file, index) in modelValue" :key="file.id ?? index" class="file-uploader__item">
|
||||
<div class="file-uploader__item-info">
|
||||
<SvgIcon name="file" :size="24" class="file-uploader__item-icon" />
|
||||
<div class="file-uploader__item-meta">
|
||||
<p class="file-uploader__item-name">{{ file.name }}</p>
|
||||
<p class="file-uploader__item-size">{{ formatFileSize(file.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-uploader__item-actions">
|
||||
<img
|
||||
v-if="file.url && isImage(file)"
|
||||
:src="file.url"
|
||||
class="file-uploader__item-thumb"
|
||||
alt="preview"
|
||||
/>
|
||||
<a v-else-if="file.url" :href="file.url" target="_blank" class="file-uploader__item-link">
|
||||
دانلود
|
||||
</a>
|
||||
|
||||
<CircleButton
|
||||
tooltip="حذف"
|
||||
bg-color="transparent"
|
||||
size="2rem"
|
||||
@click="removeFile(index, file)"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon name="close" :size="16" />
|
||||
</template>
|
||||
</CircleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="file-uploader__empty">هیچ فایلی آپلود نشده است</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import CircleButton from '@/components/CircleButton.vue'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Array, default: () => [] },
|
||||
accept: { type: String, default: '*' },
|
||||
multiple: { type: Boolean, default: true },
|
||||
maxFiles: { type: Number, default: 10 },
|
||||
maxSize: { type: Number, default: 0 },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select', 'remove', 'error'])
|
||||
|
||||
const isDragging = ref(false)
|
||||
|
||||
const uploadText = computed(() =>
|
||||
props.modelValue.length === 0 ? 'فایل جدید اضافه کنید' : 'فایل دیگری اضافه کنید'
|
||||
)
|
||||
|
||||
const handleFileSelect = (event) => {
|
||||
const files = [...event.target.files]
|
||||
emitFiles(files)
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
const files = [...event.dataTransfer.files]
|
||||
emitFiles(files)
|
||||
}
|
||||
|
||||
const emitFiles = (files) => {
|
||||
if (props.modelValue.length + files.length > props.maxFiles) {
|
||||
emit('error', `حداکثر تعداد فایل مجاز ${props.maxFiles} عدد است.`)
|
||||
return
|
||||
}
|
||||
if (props.maxSize > 0) {
|
||||
const tooBig = files.find((f) => f.size > props.maxSize)
|
||||
if (tooBig) {
|
||||
emit('error', `حجم فایل "${tooBig.name}" بیش از حد مجاز است.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
emit('select', files)
|
||||
}
|
||||
|
||||
const removeFile = (index, file) => {
|
||||
const next = [...props.modelValue]
|
||||
next.splice(index, 1)
|
||||
emit('update:modelValue', next)
|
||||
emit('remove', { index, file })
|
||||
}
|
||||
|
||||
const isImage = (file) =>
|
||||
file.type === 'image' || /\.(jpe?g|png|gif|webp|svg)$/i.test(file.name || '')
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (!bytes) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.file-uploader {
|
||||
width: 100%;
|
||||
|
||||
&__dropzone-wrap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
&__dropzone {
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
transition: border-color 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__upload-icon {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&__upload-text {
|
||||
font-family: var(--font-family-fa);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
&__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
background-color: #eee;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&__item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__item-icon {
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__item-meta {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__item-name {
|
||||
font-family: var(--font-family-fa);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__item-size {
|
||||
font-family: var(--font-family-en);
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&__item-thumb {
|
||||
width: 4rem;
|
||||
height: 3rem;
|
||||
object-fit: cover;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&__item-link {
|
||||
font-family: var(--font-family-fa);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
font-family: var(--font-family-fa);
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user