fix(uploader): resolve upload endpoint URL and update ImageUploader props in CreateProperty
deploy-chabokan / deploy (push) Canceled after 0s

This commit is contained in:
2026-08-19 04:35:49 +03:30
parent a6ece05199
commit 119a6950f0
2 changed files with 177 additions and 55 deletions
+8 -4
View File
@@ -417,8 +417,12 @@ const saveProperty = async () => {
isSaving.value = true;
try {
property.value.mainImageUrls = mainImageUrlsWithData.value.map((x) => x.url);
property.value.documentUrls = documentUrlsWithData.value.map((x) => x.url);
property.value.mainImageUrls = (mainImageUrlsWithData.value || [])
.map((x) => (typeof x === 'string' ? x : x.url || x.source || ''))
.filter(Boolean);
property.value.documentUrls = (documentUrlsWithData.value || [])
.map((x) => (typeof x === 'string' ? x : x.url || x.source || ''))
.filter(Boolean);
property.value.features = serializeFeaturesPayload(featuresValues.value);
let status = 'failed';
@@ -857,7 +861,7 @@ const saveProperty = async () => {
<i class="pi pi-file-pdf text-xl"></i>
<span>اسناد و مدارک تصویری / پی‌دی‌اف ملک</span>
</div>
<ImageUploader v-model="documentUrlsWithData" uploadUrl="/api/files/admin/upload-file" @uploaded="onDocumentUploaded" />
<ImageUploader v-model="documentUrlsWithData" bucketName="documents" :maxFiles="10" />
</div>
</div>
@@ -895,7 +899,7 @@ const saveProperty = async () => {
<i class="pi pi-image text-xl"></i>
<span>تصاویر ملک</span>
</div>
<ImageUploader v-model="mainImageUrlsWithData" uploadUrl="/api/files/admin/upload-file" @uploaded="onMainImageUploaded" />
<ImageUploader v-model="mainImageUrlsWithData" bucketName="properties" :maxFiles="10" />
</div>
</div>
</div>
+169 -51
View File
@@ -1,42 +1,48 @@
<template>
<div id="app">
<div class="image-uploader-wrapper">
<FilePond
ref="pond"
name="filepond-image"
:max-files="maxFiles"
store-as-file="true"
:allow-multiple="allowMultiple"
:accepted-file-types="acceptedFileTypes"
:server="{
process: {
url: API_URL + uploadEndpoint,
url: resolvedUploadUrl,
method: 'POST',
withCredentials: true,
headers: {
authorization: 'Bearer ' + Cookies.get('bm-token')
},
headers: authHeaders,
onload: (response) => handleProcessResponse(response)
},
revert: {
url: API_URL + deleteEndpoint,
url: resolvedDeleteUrl,
method: 'DELETE',
withCredentials: true,
headers: {
authorization: 'Bearer ' + Cookies.get('bm-token')
},
headers: authHeaders,
onload: (response) => handleRevertResponse(response)
},
load: (source, load, error, progress, abort, headers) => {
fetch(source)
.then((res) => res.blob())
.then(load)
.catch((err) => {
console.warn('FilePond failed to load existing image:', source, err);
error(err.message);
});
}
}"
label-idle="برای آپلود کلیک کنید"
label-tap-to-undo=" برای حذف کلیک کنید "
label-file-processing=" در حال آپلود "
label-file-processing-complete=" آپلود فایل تکمیل شد "
:allow-multiple="true"
v-model="uploadedFiles"
:files="initialFiles"
@removefile="handleRemoveFile"
label-idle="برای آپلود کلیک کنید یا فایل را بکشید و رها کنید"
label-tap-to-undo="برای حذف کلیک کنید"
label-file-processing="در حال آپلود"
label-file-processing-complete="آپلود فایل تکمیل شد"
/>
</div>
</template>
<script setup>
import { ref, watch, defineProps, computed } from 'vue';
import { ref, computed } from 'vue';
import Cookies from 'js-cookie';
import vueFilePond from 'vue-filepond';
import 'filepond/dist/filepond.min.css';
@@ -46,17 +52,33 @@ import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
// Define props
const props = defineProps({
bucketName: {
type: String,
default: 'properties'
},
uploadEndpoint: {
type: String,
required: true
default: ''
},
uploadUrl: {
type: String,
default: ''
},
deleteEndpoint: {
type: String,
required: true
default: ''
},
deleteUrl: {
type: String,
default: ''
},
maxFiles: {
type: Number,
default: 1
default: 10
},
allowMultiple: {
type: Boolean,
default: true
},
acceptedFiles: {
type: String,
@@ -64,58 +86,154 @@ const props = defineProps({
},
mode: {
type: String,
required: true,
default: 'public'
},
modelValue: {
type: Array,
required: false,
default: () => []
}
});
// Setup reactive variables
const API_URL = import.meta.env.VITE_API_URL;
const uploadedFiles = ref([...props.modelValue]);
const emit = defineEmits(['update:modelValue', 'upload', 'uploaded', 'delete']);
// Define FilePond component
const FilePond = vueFilePond(FilePondPluginFileValidateType, FilePondPluginImagePreview);
const emit = defineEmits(['update:modelValue', 'upload', 'delete']);
const pond = ref(null);
const acceptedFileTypes = computed(() => {
if (!props.acceptedFiles) return undefined;
return props.acceptedFiles.split(',').map((s) => s.trim());
});
const API_URL = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
const authHeaders = computed(() => {
const token = Cookies.get('bm-token') || (typeof localStorage !== 'undefined' ? localStorage.getItem('bm-token') : '') || '';
return token ? { authorization: `Bearer ${token}` } : {};
});
const resolvedUploadUrl = computed(() => {
let endpoint = props.uploadUrl || props.uploadEndpoint;
if (!endpoint || endpoint === '/api/files/admin/upload-file') {
endpoint = `/api/files/bucket/upload-single-filepond/${props.bucketName}`;
}
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
return endpoint;
}
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${API_URL}${cleanEndpoint}`;
});
const resolvedDeleteUrl = computed(() => {
let endpoint = props.deleteUrl || props.deleteEndpoint;
if (!endpoint) {
endpoint = `/api/files/bucket/delete-file/${props.bucketName}`;
}
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
return endpoint;
}
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${API_URL}${cleanEndpoint}`;
});
const initialFiles = computed(() => {
if (!Array.isArray(props.modelValue)) return [];
return props.modelValue
.map((item) => {
const url = typeof item === 'string' ? item : item.url || item.source || '';
if (!url) return null;
return {
source: url,
options: {
type: 'local'
}
};
})
.filter(Boolean);
});
// Handle server responses for file processing
function handleProcessResponse(response) {
if (props.mode === 'public') {
const data = JSON.parse(response);
const publicUrl = data.data[0].fileRecord.publicUrl;
const fileName = data.data[0].fileRecord.fileName;
uploadedFiles.value.push(publicUrl);
emit('update:modelValue', uploadedFiles);
emit('upload', uploadedFiles);
return publicUrl;
} else {
const data = JSON.parse(response);
const privateUrl = data.data[0].fileRecord.privateUrl;
const fileName = data.data[0].fileRecord.fileName;
uploadedFiles.value.push(privateUrl);
emit('update:modelValue', uploadedFiles);
emit('upload', uploadedFiles);
return privateUrl;
let parsed;
try {
parsed = typeof response === 'string' ? JSON.parse(response) : response;
} catch (e) {
console.error('Failed to parse file upload response:', e);
return '';
}
let fileRecord = null;
if (Array.isArray(parsed?.data)) {
fileRecord = parsed.data[0]?.fileRecord || parsed.data[0];
} else if (parsed?.data?.fileRecord) {
fileRecord = parsed.data.fileRecord;
} else if (parsed?.data) {
fileRecord = parsed.data;
} else {
fileRecord = parsed;
}
const fileUrl =
props.mode === 'private'
? fileRecord?.privateUrl || fileRecord?.fileURL || fileRecord?.url || ''
: fileRecord?.publicUrl || fileRecord?.fileURL || fileRecord?.url || '';
const fileName = fileRecord?.fileName || fileRecord?.name || (fileUrl ? fileUrl.split('/').pop() : 'file');
const newItem = {
name: fileName,
url: fileUrl,
progress: 100,
bucketName: props.bucketName
};
const currentList = Array.isArray(props.modelValue) ? [...props.modelValue] : [];
const isStringArray = currentList.length > 0 && typeof currentList[0] === 'string';
currentList.push(isStringArray ? fileUrl : newItem);
emit('update:modelValue', currentList);
emit('upload', newItem);
emit('uploaded', newItem);
return fileName || fileUrl;
}
// Handle server responses for file revert
function handleRevertResponse(response) {
const data = JSON.parse(response);
const fileName = data.data;
// Find and remove the file name from uploadedFiles
const fileIndex = uploadedFiles.value.findIndex((name) => name === fileName);
if (fileIndex !== -1) {
uploadedFiles.value.splice(fileIndex, 1);
emit('update:modelValue', uploadedFiles);
emit('delete', uploadedFiles);
let deletedIdentifier = '';
try {
const parsed = typeof response === 'string' ? JSON.parse(response) : response;
deletedIdentifier = parsed?.data || parsed?.fileName || response;
} catch (e) {
deletedIdentifier = response;
}
return fileName;
removeFromFileList(deletedIdentifier);
emit('delete', deletedIdentifier);
return deletedIdentifier;
}
function handleRemoveFile(error, fileItem) {
if (error) return;
const identifier = fileItem?.serverId || fileItem?.source || fileItem?.filename || '';
if (identifier) {
removeFromFileList(identifier);
}
}
function removeFromFileList(identifier) {
if (!Array.isArray(props.modelValue) || !identifier) return;
const updated = props.modelValue.filter((item) => {
if (typeof item === 'string') {
return item !== identifier && !item.includes(identifier);
}
if (typeof item === 'object' && item !== null) {
const u = item.url || item.source || item.name || '';
return u !== identifier && !u.includes(identifier);
}
return true;
});
emit('update:modelValue', updated);
}
</script>