feat: implement media download functionality and enhance case handling in reports
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import FileUpload from 'primevue/fileupload';
|
||||
import Badge from 'primevue/badge';
|
||||
import { formatFileSize } from '../../../utils/fileUtils';
|
||||
import { convertEnglishToPersian } from '../../../utils/numberUtils';
|
||||
|
||||
const files = defineModel({ type: Array, default: () => [] });
|
||||
|
||||
@@ -13,56 +13,106 @@ defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const MAX_FILE_SIZE = 10485760;
|
||||
|
||||
const fileUpload = ref(null);
|
||||
const dragging = ref(false);
|
||||
|
||||
const totalSize = computed(() => files.value.reduce((sum, file) => sum + file.size, 0));
|
||||
|
||||
const openFileDialog = () => {
|
||||
fileUpload.value.choose();
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedFiles = (event) => {
|
||||
files.value = event.files;
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveFile = (removeFileCallback, index) => {
|
||||
removeFileCallback(index);
|
||||
files.value = fileUpload.value.files;
|
||||
}
|
||||
};
|
||||
|
||||
const onClearFiles = (clearCallback) => {
|
||||
clearCallback();
|
||||
files.value = [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FileUpload
|
||||
ref="fileUpload"
|
||||
class="w-full !border-dashed !border-[#C1D0E8] !min-h-[100px] flex items-center justify-center"
|
||||
name="files[]"
|
||||
:customUpload="true"
|
||||
:showUploadButton="false"
|
||||
:showCancelButton="false"
|
||||
:showChooseButton="false"
|
||||
:multiple="true"
|
||||
accept="image/*"
|
||||
:maxFileSize="10485760"
|
||||
@select="onSelectedFiles"
|
||||
<div
|
||||
class="ui-file-upload"
|
||||
:class="{ 'ui-file-upload--dragging': dragging, 'ui-file-upload--filled': files.length }"
|
||||
@dragover="dragging = true"
|
||||
@dragleave="dragging = false"
|
||||
@drop="dragging = false"
|
||||
>
|
||||
<template #content="{ files: selectedFiles, removeFileCallback }">
|
||||
<div v-if="selectedFiles.length > 0" class="flex flex-wrap gap-4 pt-4">
|
||||
<div v-for="(file, index) of selectedFiles" :key="file.name + file.type + file.size" class="relative w-[5rem] h-[5rem] rounded-lg border border-gray-200 flex flex-col items-center overflow-hidden">
|
||||
<img role="presentation" :alt="file.name" :src="file.objectURL" class="absolute top-0 left-0 w-full h-full object-cover"/>
|
||||
<div class="absolute bottom-0 left-0 w-full flex flex-col items-center gap-1 bg-white/80 p-1">
|
||||
<span class="w-full text-[10px] font-[400] text-center text-ellipsis whitespace-nowrap overflow-hidden">{{ file.name }}</span>
|
||||
<span class="text-[9px]">{{ formatFileSize(file.size) }}</span>
|
||||
<FileUpload
|
||||
ref="fileUpload"
|
||||
name="files[]"
|
||||
:customUpload="true"
|
||||
:showUploadButton="false"
|
||||
:showCancelButton="false"
|
||||
:multiple="true"
|
||||
accept="image/*"
|
||||
:maxFileSize="MAX_FILE_SIZE"
|
||||
@select="onSelectedFiles"
|
||||
>
|
||||
<template #content="{ files: selectedFiles, removeFileCallback, clearCallback }">
|
||||
<div v-if="selectedFiles.length > 0" class="flex flex-col gap-3">
|
||||
<ul class="grid grid-cols-[repeat(auto-fill,5rem)] gap-2">
|
||||
<li
|
||||
v-for="(file, index) of selectedFiles"
|
||||
:key="file.name + file.type + file.size"
|
||||
class="group relative aspect-square rounded-xl overflow-hidden border border-[#DEE3EB] bg-[#F8FAFC]"
|
||||
>
|
||||
<img role="presentation" :alt="file.name" :src="file.objectURL" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-x-0 bottom-0 px-2 pb-1.5 pt-4 bg-gradient-to-t from-[#2F343D]/80 to-transparent text-white">
|
||||
<p class="text-[10px] font-[600] truncate" dir="ltr">{{ file.name }}</p>
|
||||
<p class="text-[9px] font-normal opacity-80" dir="ltr">{{ formatFileSize(file.size) }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-1.5 left-1.5 w-5 h-5 flex items-center justify-center rounded-full bg-white/90 text-[#7A1419] shadow-sm transition-opacity opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||
:aria-label="`حذف ${file.name}`"
|
||||
@click.stop="onRemoveFile(removeFileCallback, index)"
|
||||
>
|
||||
<i class="fi fi-rr-cross-small text-[12px] leading-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full aspect-square rounded-xl border border-dashed border-[#C1D0E8] bg-white flex flex-col items-center justify-center gap-1 text-[#365891] transition-colors hover:border-[#365891] hover:bg-[#EEF2F8]"
|
||||
@click="openFileDialog"
|
||||
>
|
||||
<i class="fi fi-rr-plus text-[16px] leading-none"></i>
|
||||
<span class="text-[10px] font-[600]">افزودن</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex items-center justify-between gap-2 border-t border-[#EBEBEB] pt-2">
|
||||
<p class="text-[11px] font-normal text-[#576989]">
|
||||
{{ convertEnglishToPersian(String(selectedFiles.length)) }} فایل · <span dir="ltr">{{ formatFileSize(totalSize) }}</span>
|
||||
</p>
|
||||
<button type="button" class="text-[11px] font-[600] text-[#7A1419]" @click="onClearFiles(clearCallback)">حذف همه</button>
|
||||
</div>
|
||||
<button type="button" class="absolute top-1 left-1 w-4 h-4 flex items-center justify-center rounded-full bg-white text-[#7A1419] text-[10px] cursor-pointer" @click.stop="onRemoveFile(removeFileCallback, index)">✕</button>
|
||||
</div>
|
||||
<button type="button" class="w-[5rem] h-[5rem] rounded-lg border border-dashed border-[#C1D0E8] text-[11px] text-[#365891] cursor-pointer" @click="openFileDialog">افزودن</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="w-full flex items-center justify-center flex-col cursor-pointer" @click="openFileDialog">
|
||||
<p class="text-[11px] font-[600] text-[#365891]">{{ title }}</p>
|
||||
<p class="mt-1 mb-0 text-[11px] font-[400] text-[#365891]">
|
||||
جهت آپلود تصویر کلیک کنید و یا فایل خود را کشیده و در این قسمت رها (Drag and Drop) کنید.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</FileUpload>
|
||||
</template>
|
||||
<template #empty>
|
||||
<button type="button" class="ui-file-upload-dropzone" @click="openFileDialog">
|
||||
<span class="ui-file-upload-icon">
|
||||
<i class="fi fi-rr-cloud-upload text-[20px] leading-none"></i>
|
||||
</span>
|
||||
<span class="text-[12px] font-[700] text-[#3E4C63]">{{ title }}</span>
|
||||
<span class="text-[11px] font-normal text-[#576989] leading-5">
|
||||
فایل را اینجا رها کنید یا برای انتخاب کلیک کنید
|
||||
</span>
|
||||
<span class="mt-1 text-[10px] font-normal text-[#576989] bg-white border border-[#DEE3EB] rounded-md px-2 py-0.5">
|
||||
JPG، PNG · حداکثر {{ formatFileSize(MAX_FILE_SIZE) }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</FileUpload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,8 +23,21 @@ const walletBalance = computed(() => Number(pageProps.value.auth?.wallet_balance
|
||||
const amount = ref('');
|
||||
const selectedCardId = ref(props.bankCards[0]?.id ?? null);
|
||||
|
||||
const MIN_WITHDRAW_AMOUNT = 10000;
|
||||
|
||||
const numericAmount = computed(() => Number(convertPersianToEnglish(amount.value).replace(/\D/g, '')) || 0);
|
||||
|
||||
const selectedCard = computed(() => props.bankCards.find((card) => card.id === selectedCardId.value) ?? null);
|
||||
|
||||
const amountError = computed(() => {
|
||||
if (!amount.value) return '';
|
||||
if (numericAmount.value < MIN_WITHDRAW_AMOUNT) return `حداقل مبلغ برداشت ${formatPrice(MIN_WITHDRAW_AMOUNT)} تومان است.`;
|
||||
if (numericAmount.value > walletBalance.value) return 'مبلغ برداشت از موجودی کیف پول بیشتر است.';
|
||||
return '';
|
||||
});
|
||||
|
||||
const canSubmit = computed(() => Boolean(selectedCard.value) && numericAmount.value >= MIN_WITHDRAW_AMOUNT && !amountError.value);
|
||||
|
||||
const formatCardNumber = (cardNumber) => convertEnglishToPersian(cardNumber.replace(/(\d{4})(?=\d)/g, '$1 - '));
|
||||
|
||||
const selectFullBalance = () => {
|
||||
@@ -37,15 +50,7 @@ const withdrawForm = useForm({
|
||||
});
|
||||
|
||||
const submitWithdraw = () => {
|
||||
if (numericAmount.value < 10000) {
|
||||
toast.error('حداقل مبلغ برداشت ۱۰٬۰۰۰ تومان است.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedCardId.value) {
|
||||
toast.error('لطفا کارت بانکی مقصد را انتخاب کنید.');
|
||||
return;
|
||||
}
|
||||
if (!canSubmit.value) return;
|
||||
|
||||
withdrawForm.amount = numericAmount.value;
|
||||
withdrawForm.bank_card_id = selectedCardId.value;
|
||||
@@ -95,72 +100,99 @@ const deleteCard = (card) => {
|
||||
|
||||
<template>
|
||||
<UiCard title="درخواست برداشت" variant="default">
|
||||
<div class="grid grid-cols-2 max-lg:grid-cols-1 gap-6 items-start">
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-[11px] font-normal text-[#576989] leading-6">
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="flex items-start justify-between gap-4 max-md:flex-col">
|
||||
<p class="text-[11px] font-normal text-[#576989] leading-6 max-w-xl">
|
||||
مبلغ مورد نظر پس از بررسی و تایید توسط کارشناسان، از کیف پول شما کسر و به کارت انتخابی واریز میشود.
|
||||
</p>
|
||||
<div class="flex items-end gap-3 max-sm:flex-col max-sm:items-stretch">
|
||||
<div class="flex items-center gap-2 bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-2 shrink-0">
|
||||
<span class="text-[11px] font-normal text-[#576989]">موجودی قابل برداشت:</span>
|
||||
<span class="text-[12px] font-semibold text-[#3E4C63]">{{ formatPrice(walletBalance) }} تومان</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="flex flex-col gap-3 border-t border-[#EBEBEB] pt-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-[12px] font-semibold text-[#3E4C63]">۱. کارت بانکی مقصد</h3>
|
||||
<button v-if="bankCards.length" type="button" class="text-[11px] font-semibold text-[#365891]" @click="cardModalVisible = true">
|
||||
+ افزودن کارت
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!bankCards.length" class="flex flex-col items-center gap-3 bg-[#F8FAFC] border border-dashed border-[#C1D0E8] rounded-xl p-5">
|
||||
<p class="text-[11px] font-normal text-[#576989] text-center leading-6">
|
||||
هنوز کارتی ثبت نکردهاید. برای ثبت درخواست برداشت ابتدا یک کارت بانکی اضافه کنید.
|
||||
</p>
|
||||
<UiButton label="افزودن کارت بانکی" severity="secondary" size="small" @click="cardModalVisible = true" />
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-2 max-lg:grid-cols-1 gap-3">
|
||||
<label
|
||||
v-for="card in bankCards"
|
||||
:key="card.id"
|
||||
class="flex items-center gap-3 border rounded-xl px-4 py-3 cursor-pointer transition-colors"
|
||||
:class="selectedCardId === card.id ? 'border-[#365891] bg-[#EEF2F8]' : 'border-[#DEE3EB] bg-white hover:bg-[#F8FAFC]'"
|
||||
>
|
||||
<input
|
||||
v-model="selectedCardId"
|
||||
type="radio"
|
||||
name="bank_card"
|
||||
:value="card.id"
|
||||
class="accent-[#365891]"
|
||||
/>
|
||||
<span class="flex flex-col gap-1 flex-1">
|
||||
<span class="text-[12px] font-semibold text-[#3E4C63]" dir="ltr">{{ formatCardNumber(card.card_number) }}</span>
|
||||
<span class="text-[11px] font-normal text-[#576989]">{{ card.holder_name }}</span>
|
||||
</span>
|
||||
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click.prevent="deleteCard(card)">حذف</button>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-3 border-t border-[#EBEBEB] pt-4">
|
||||
<h3 class="text-[12px] font-semibold text-[#3E4C63]">۲. مبلغ برداشت</h3>
|
||||
<div class="flex flex-col gap-2 max-w-md">
|
||||
<UiInput
|
||||
v-model="amount"
|
||||
label="مبلغ برداشت"
|
||||
placeholder="مبلغ مورد نظر را به تومان وارد کنید"
|
||||
prefixEnd="تومان"
|
||||
prefixClasses="!border-dominant-50"
|
||||
class="!w-full"
|
||||
customClass="!border-dominant-50 !font-normal !text-[12px]"
|
||||
:error="amountError"
|
||||
/>
|
||||
<div class="flex items-center justify-between gap-2 flex-wrap">
|
||||
<span class="text-[11px] font-normal text-[#576989]">حداقل مبلغ برداشت {{ formatPrice(MIN_WITHDRAW_AMOUNT) }} تومان است.</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-[11px] font-normal text-[#576989] bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-1.5 hover:bg-[#EEF2F8] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="walletBalance < MIN_WITHDRAW_AMOUNT"
|
||||
@click="selectFullBalance"
|
||||
>
|
||||
برداشت کل موجودی
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="text-[11px] font-normal text-[#576989] bg-[#F5F7FA] border border-[#DEE3EB] rounded-lg px-3 py-1.5 hover:bg-[#EEF2F8]"
|
||||
@click="selectFullBalance"
|
||||
>
|
||||
کل موجودی ({{ formatPrice(walletBalance) }} تومان)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 max-md:flex-col max-md:items-stretch border-t border-[#EBEBEB] pt-4">
|
||||
<p v-if="canSubmit" class="text-[11px] font-normal text-[#576989] leading-6">
|
||||
<span class="font-[600] text-[#7A1419]">{{ formatPrice(numericAmount) }} تومان</span>
|
||||
به کارت
|
||||
<span class="font-[600] text-[#3E4C63]" dir="ltr">{{ formatCardNumber(selectedCard.card_number) }}</span>
|
||||
واریز میشود.
|
||||
</p>
|
||||
<p v-else class="text-[11px] font-normal text-[#576989] leading-6">
|
||||
برای ثبت درخواست، کارت مقصد را انتخاب و مبلغ را وارد کنید.
|
||||
</p>
|
||||
<UiButton
|
||||
label="ثبت درخواست برداشت"
|
||||
severity="success"
|
||||
size="small"
|
||||
class="self-start"
|
||||
class="shrink-0"
|
||||
:loading="withdrawForm.processing"
|
||||
:disabled="withdrawForm.processing"
|
||||
:disabled="withdrawForm.processing || !canSubmit"
|
||||
@click="submitWithdraw"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[11px] font-semibold text-[#3E4C63]">کارت بانکی مقصد</span>
|
||||
<button type="button" class="text-[11px] font-semibold text-[#365891]" @click="cardModalVisible = true">
|
||||
+ افزودن کارت
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!bankCards.length" class="bg-[#F8FAFC] border border-dashed border-[#C1D0E8] rounded-xl p-4 text-[11px] font-normal text-[#576989] text-center leading-6">
|
||||
هنوز کارتی ثبت نکردهاید. برای ثبت درخواست برداشت ابتدا یک کارت بانکی اضافه کنید.
|
||||
</p>
|
||||
<label
|
||||
v-for="card in bankCards"
|
||||
:key="card.id"
|
||||
class="flex items-center gap-3 border rounded-xl px-4 py-3 cursor-pointer transition-colors"
|
||||
:class="selectedCardId === card.id ? 'border-[#365891] bg-[#EEF2F8]' : 'border-[#DEE3EB] bg-white hover:bg-[#F8FAFC]'"
|
||||
>
|
||||
<input
|
||||
v-model="selectedCardId"
|
||||
type="radio"
|
||||
name="bank_card"
|
||||
:value="card.id"
|
||||
class="accent-[#365891]"
|
||||
/>
|
||||
<span class="flex flex-col gap-1 flex-1">
|
||||
<span class="text-[12px] font-semibold text-[#3E4C63]" dir="ltr">{{ formatCardNumber(card.card_number) }}</span>
|
||||
<span class="text-[11px] font-normal text-[#576989]">{{ card.holder_name }}</span>
|
||||
</span>
|
||||
<button type="button" class="text-[11px] font-semibold text-[#7A1419]" @click.prevent="deleteCard(card)">حذف</button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="cardModalVisible" :modal="true" :closable="true" :dismissableMask="false" :style="{ width: '420px' }" class="fast-request-dialog" :pt="{ header: '!bg-client-primaryBlue' }">
|
||||
|
||||
Reference in New Issue
Block a user